RSA шифрование в PHP (openssl), Android-Java, JavaScript и Go

в 8:49, , рубрики: android, bouncycastle, Go, golang, java, java 8, javascript, jsencrypt, openssl, php, криптография, Разработка под android

RSA — это алгоритм шифрования с открытым ключем. Шифрование с открытым ключем весьма полезная вещь. RSA позволяет создать два ключа: открытый и закрытый. Разместить открытый ключ где-то и им шифровать, а расшифровать сможет только обладатель закрытого ключа.

Например, мы можем сделать веб магазин на ПХП, который будет принимать заказы с данными кредитных карт. Магазин на ПХП будет шифровать данные кредитных карт открытым ключем. Сам пхп-магазин расшифровать эти зашифрованные данные уже не сможет. Хорошее решение, хакер неожиданно так взломает веб магазин (написанный на ПХП), а карты зашифрованы.

Но как же владелец сайта будет получать доступ к картам? Ему нужно взять шифротекст, закрытый ключ и расшифровать. Можно представить, что закрытый ключ будет храниться в телефоне, и телефон может вытянуть шифротекст с админки через QR код. Но в разных языках реализация криптографии немного отличается, и моя статья как раз о том, как зашифровать текст на одном языке програмирования, а расшифровать на другом языке.

В чем могут быть отличия?
— как хранятся ключи;
— как хранится шифротекст: бинарная форма или в кодировке base64;
— паддинг.

Первым делом нам нужны ключи. Я предлагаю их создать с помощью openssl

openssl genrsa -out private.pem 512
openssl rsa -in private.pem -out public.pem -outform PEM -pubout

Для экономии места на экране я выбрал 512 бит, целесообразно использовать 1024 или 2048 бит. Например, SSL gitgub.com использует 2048.

Так же размер ключа определяет максимальный объем данных, которые вы можете зашифровать, но с учетом того, что мы будем использовать OPENSSL_PKCS1_PADDING (по умолчанию в ПХП), то из размера ключа следует вычесть 11 байт и при ключе 512 бит мы можем зашифровать 53 байта. Не использовать паддинг вообще опасно, если вы не знаете зачем он нужен.

Теперь у нас есть private.pem и public.pem. Эти ключи в текстовом формате, и их будет достаточно удобно использовать в примерах. Я хочу, чтобы каждая программа состояла из одного файла, так будет наглядней.

private.pem

-----BEGIN RSA PRIVATE KEY-----
MIIBPQIBAAJBALqbHeRLCyOdykC5SDLqI49ArYGYG1mqaH9/GnWjGavZM02fos4l
c2w6tCchcUBNtJvGqKwhC5JEnx3RYoSX2ucCAwEAAQJBAKn6O+tFFDt4MtBsNcDz
GDsYDjQbCubNW+yvKbn4PJ0UZoEebwmvH1ouKaUuacJcsiQkKzTHleu4krYGUGO1
mEECIQD0dUhj71vb1rN1pmTOhQOGB9GN1mygcxaIFOWW8znLRwIhAMNqlfLijUs6
rY+h1pJa/3Fh1HTSOCCCCWA0NRFnMANhAiEAwddKGqxPO6goz26s2rHQlHQYr47K
vgPkZu2jDCo7trsCIQC/PSfRsnSkEqCX18GtKPCjfSH10WSsK5YRWAY3KcyLAQIh
AL70wdUu5jMm2ex5cZGkZLRB50yE6rBiHCd5W1WdTFoe
-----END RSA PRIVATE KEY-----

public.pem

-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALqbHeRLCyOdykC5SDLqI49ArYGYG1mq
aH9/GnWjGavZM02fos4lc2w6tCchcUBNtJvGqKwhC5JEnx3RYoSX2ucCAwEAAQ==
-----END PUBLIC KEY-----

Начнем с ПХП

encode.php

<?php
$pub = <<<SOMEDATA777
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALqbHeRLCyOdykC5SDLqI49ArYGYG1mq
aH9/GnWjGavZM02fos4lc2w6tCchcUBNtJvGqKwhC5JEnx3RYoSX2ucCAwEAAQ==
-----END PUBLIC KEY-----
SOMEDATA777;
$data = "PHP is my secret love.";
$pk  = openssl_get_publickey($pub);
openssl_public_encrypt($data, $encrypted, $pk);
echo chunk_split(base64_encode($encrypted));
?>

goo.gl/Xb7ayw

Получим, что-то типа (вы будете получать новый уникальный шифротекст при каждой попытке зашифровать текст):

JutBa0GLHzGrlygxwWr66cizw4W4za+DbzZweNM0iloCD7xEP9LclL013lcksJL5XhjW44U+oxpq
cX1ZSLhWuA==

decode.php

<?php
$key = <<<SOMEDATA777
-----BEGIN RSA PRIVATE KEY-----
MIIBPQIBAAJBALqbHeRLCyOdykC5SDLqI49ArYGYG1mqaH9/GnWjGavZM02fos4l
c2w6tCchcUBNtJvGqKwhC5JEnx3RYoSX2ucCAwEAAQJBAKn6O+tFFDt4MtBsNcDz
GDsYDjQbCubNW+yvKbn4PJ0UZoEebwmvH1ouKaUuacJcsiQkKzTHleu4krYGUGO1
mEECIQD0dUhj71vb1rN1pmTOhQOGB9GN1mygcxaIFOWW8znLRwIhAMNqlfLijUs6
rY+h1pJa/3Fh1HTSOCCCCWA0NRFnMANhAiEAwddKGqxPO6goz26s2rHQlHQYr47K
vgPkZu2jDCo7trsCIQC/PSfRsnSkEqCX18GtKPCjfSH10WSsK5YRWAY3KcyLAQIh
AL70wdUu5jMm2ex5cZGkZLRB50yE6rBiHCd5W1WdTFoe
-----END RSA PRIVATE KEY-----
SOMEDATA777;
$data = "JutBa0GLHzGrlygxwWr66cizw4W4za+DbzZweNM0iloCD7xEP9LclL013lcksJL5XhjW44U+oxpq cX1ZSLhWuA==";
$pk  = openssl_get_privatekey($key);
openssl_private_decrypt(base64_decode($data), $out, $pk);
echo $out;
?>

goo.gl/0CWTQ9

Теперь на Go

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"fmt"
	"strings"
)

func main() {
	b64 := `JutBa0GLHzGrlygxwWr66cizw4W4za+DbzZweNM0iloCD7xEP9LclL013lcksJL5XhjW44U+oxpq
cX1ZSLhWuA==
`
	b1, err := Base64Dec(b64)
	if err != nil {
		panic(err)
	}
	b2, err := RsaDecrypt(b1, privateKey)
	fmt.Println(string(b2), err)

	b1, err = RsaEncrypt([]byte("Go the best language"), publicKey)
	if err != nil {
		panic(err)
	}
	s1 := Base64Enc(b1)
	fmt.Println(s1)
	b1, err = Base64Dec(s1)
	b2, err = RsaDecrypt(b1, privateKey)
	fmt.Println(string(b2), err)

}

func Base64Enc(b1 []byte) string {
	s1 := base64.StdEncoding.EncodeToString(b1)
	s2 := ""
	var LEN int = 76
	for len(s1) > 76 {
		s2 = s2 + s1[:LEN] + "n"
		s1 = s1[LEN:]
	}
	s2 = s2 + s1
	return s2
}

func Base64Dec(s1 string) ([]byte, error) {
	s1 = strings.Replace(s1, "n", "", -1)
	s1 = strings.Replace(s1, "r", "", -1)
	s1 = strings.Replace(s1, " ", "", -1)
	return base64.StdEncoding.DecodeString(s1)
}

func RsaDecrypt(ciphertext []byte, key []byte) ([]byte, error) {
	block, _ := pem.Decode(key)
	if block == nil {
		return nil, errors.New("private key error!")
	}
	priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}

func RsaEncrypt(origData []byte, key []byte) ([]byte, error) {
	block, _ := pem.Decode(key)
	if block == nil {
		return nil, errors.New("public key error")
	}
	pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	pub := pubInterface.(*rsa.PublicKey)
	return rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
}

var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALqbHeRLCyOdykC5SDLqI49ArYGYG1mq
aH9/GnWjGavZM02fos4lc2w6tCchcUBNtJvGqKwhC5JEnx3RYoSX2ucCAwEAAQ==
-----END PUBLIC KEY-----
`)

var privateKey = []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIIBPQIBAAJBALqbHeRLCyOdykC5SDLqI49ArYGYG1mqaH9/GnWjGavZM02fos4l
c2w6tCchcUBNtJvGqKwhC5JEnx3RYoSX2ucCAwEAAQJBAKn6O+tFFDt4MtBsNcDz
GDsYDjQbCubNW+yvKbn4PJ0UZoEebwmvH1ouKaUuacJcsiQkKzTHleu4krYGUGO1
mEECIQD0dUhj71vb1rN1pmTOhQOGB9GN1mygcxaIFOWW8znLRwIhAMNqlfLijUs6
rY+h1pJa/3Fh1HTSOCCCCWA0NRFnMANhAiEAwddKGqxPO6goz26s2rHQlHQYr47K
vgPkZu2jDCo7trsCIQC/PSfRsnSkEqCX18GtKPCjfSH10WSsK5YRWAY3KcyLAQIh
AL70wdUu5jMm2ex5cZGkZLRB50yE6rBiHCd5W1WdTFoe
-----END RSA PRIVATE KEY-----
`)

play.golang.org/p/nsyAw5kYDt

Go Playground всегда дают одни и те же случайные числа и поэтому результат:

aOleRSXhBT1XR7Al9cxdmM/8KnM2CvQdnNqnvwtq1ivFJ1aITxJUCuTw8ZRB8mY+elhoiUmC4UjM
mwyTKmjqQw==

JavaScript шифрование

Шифровать я буду с помощью jsEncrypt:

$(function () {
  $('#but').click(function(){
     var pub = $('#pub').val();
     var crypt = new JSEncrypt();
     crypt.setPublicKey(pub);
     var data = $('#data').val();
     $('#out').val(crypt.encrypt(data));
  });
});

cossackpyra.github.io/april14/html/encrypt.html

И получил:

C2uWXwp6OsxLKnr3cXpJIf/RcPzgjlxNXj8IX2R47binEo2dLFhJISDnOioQaM8kAl/lqSSOCLdrYP12Tc/YXQ==

$(function () {
  $('#but').click(function(){
     var key = $('#key').val();
     var crypt = new JSEncrypt();
     crypt.setPrivateKey(key);
     var data = $('#data').val();
     $('#out').val(crypt.decrypt(data));
  });
});

cossackpyra.github.io/april14/html/decrypt.html

Android не Java

Java — это так много всего, и ни фига нет.

В Android есть android.util.Base64, а Java 8 java.util.Base64, а еще есть org.apache.commons.codec.binary.Base64.

Java не умеет читать сертификаты в PEM формате, какие задачи и цели ставило руководство перед создателями java.security и javax.crypto — это мрак, но явно не экономить место на диске.

В Bouncy Castle есть PEMParser. Но Bouncy Castle в онлайн редактор не подцепишь, а в Android используется непонятно какой Bouncy Castle. Поэтому есть Spongy Castle, но это уже будет другая графа в реализации криптографии в форме «Supplement No. 5 to Part 742—Encryption Registration» на пункты 6 и 7 уже не ответишь «нет, нет».

SNAP-R

(6) Do the products incorporate encryption components produced or furnished by non-U.S. sources or vendors? (If unsure, please explain.)

No.

(7) With respect to your company's encryption products, are any of them manufactured outside the United States? If yes, provide manufacturing locations. (Insert “not applicable”, if you are not the principal producer of encryption products.)

No.

Поэтому из private.pem можно вынуть модуль, приватную и публичную экспоненты. (Это допустимо, я изначально ключи создал с помощью openssl.)

openssl rsa -in private.pem -text -noout
Private-Key: (512 bit)
modulus:
    00:ba:9b:1d:e4:4b:0b:23:9d:ca:40:b9:48:32:ea:
    23:8f:40:ad:81:98:1b:59:aa:68:7f:7f:1a:75:a3:
    19:ab:d9:33:4d:9f:a2:ce:25:73:6c:3a:b4:27:21:
    71:40:4d:b4:9b:c6:a8:ac:21:0b:92:44:9f:1d:d1:
    62:84:97:da:e7
publicExponent: 65537 (0x10001)
privateExponent:
    00:a9:fa:3b:eb:45:14:3b:78:32:d0:6c:35:c0:f3:
    18:3b:18:0e:34:1b:0a:e6:cd:5b:ec:af:29:b9:f8:
    3c:9d:14:66:81:1e:6f:09:af:1f:5a:2e:29:a5:2e:
    69:c2:5c:b2:24:24:2b:34:c7:95:eb:b8:92:b6:06:
    50:63:b5:98:41
prime1:
    00:f4:75:48:63:ef:5b:db:d6:b3:75:a6:64:ce:85:
    03:86:07:d1:8d:d6:6c:a0:73:16:88:14:e5:96:f3:
    39:cb:47
prime2:
    00:c3:6a:95:f2:e2:8d:4b:3a:ad:8f:a1:d6:92:5a:
    ff:71:61:d4:74:d2:38:20:82:09:60:34:35:11:67:
    30:03:61
exponent1:
    00:c1:d7:4a:1a:ac:4f:3b:a8:28:cf:6e:ac:da:b1:
    d0:94:74:18:af:8e:ca:be:03:e4:66:ed:a3:0c:2a:
    3b:b6:bb
exponent2:
    00:bf:3d:27:d1:b2:74:a4:12:a0:97:d7:c1:ad:28:
    f0:a3:7d:21:f5:d1:64:ac:2b:96:11:58:06:37:29:
    cc:8b:01
coefficient:
    00:be:f4:c1:d5:2e:e6:33:26:d9:ec:79:71:91:a4:
    64:b4:41:e7:4c:84:ea:b0:62:1c:27:79:5b:55:9d:
    4c:5a:1e

В Java это будет выглядеть так:

// Private key
		BigInteger modulus = new BigInteger(
				"BA9B1DE44B0B239DCA40B94832EA238F40AD81981B59AA687F7F1A75A319ABD9334D9FA2CE25736C3AB4272171404DB49BC6A8AC210B92449F1DD1628497DAE7",
				16);
		BigInteger exp = new BigInteger(
				"00a9fa3beb45143b7832d06c35c0f3183b180e341b0ae6cd5becaf29b9f83c9d1466811e6f09af1f5a2e29a52e69c25cb224242b34c795ebb892b6065063b59841",
				16);

//Public Key

		BigInteger modulus = new BigInteger(
				"BA9B1DE44B0B239DCA40B94832EA238F40AD81981B59AA687F7F1A75A319ABD9334D9FA2CE25736C3AB4272171404DB49BC6A8AC210B92449F1DD1628497DAE7",
				16);
		BigInteger pubExp = new BigInteger("010001", 16);


Вся программа на Java 8:

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;

import java.util.Base64;

//import javax.xml.bind.DatatypeConverter;

public class HelloWorld {

	public static void main(String[] args) throws Exception {
		try {
			byte[] b1 = decrypt("JutBa0GLHzGrlygxwWr66cizw4W4za+DbzZweNM0iloCD7xEP9LclL013lcksJL5XhjW44nU+oxpqcX1ZSLhWuA==");
			String s1 = new String(b1, "UTF-8");
			System.out.println(s1);
			byte[] b2 = encrypt("Java kills".getBytes("UTF-8"));
			String s2 = Base64.getEncoder().encodeToString(b2);
			System.out.println(s2);
			byte[] b3 = decrypt(s2);
			String s3 = new String(b3, "UTF-8");
			System.out.println(s3);

		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static byte[] decrypt(String key) throws Exception {

		BigInteger modulus = new BigInteger(
				"BA9B1DE44B0B239DCA40B94832EA238F40AD81981B59AA687F7F1A75A319ABD9334D9FA2CE25736C3AB4272171404DB49BC6A8AC210B92449F1DD1628497DAE7",
				16);
		BigInteger exp = new BigInteger(
				"00a9fa3beb45143b7832d06c35c0f3183b180e341b0ae6cd5becaf29b9f83c9d1466811e6f09af1f5a2e29a52e69c25cb224242b34c795ebb892b6065063b59841",
				16);

		RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(modulus, exp);
		KeyFactory kf = KeyFactory.getInstance("RSA");
		PrivateKey privKey = kf.generatePrivate(keySpec);

		Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		cipher.init(Cipher.DECRYPT_MODE, privKey);

		byte[] decodedStr = Base64.getDecoder().decode(
				key.replace("n", "").replace("r", "").replace(" ", ""));
		byte[] plainText = cipher.doFinal(decodedStr);

		return plainText;
	}

	private static byte[] encrypt(byte[] b1) throws Exception {
		BigInteger modulus = new BigInteger(
				"BA9B1DE44B0B239DCA40B94832EA238F40AD81981B59AA687F7F1A75A319ABD9334D9FA2CE25736C3AB4272171404DB49BC6A8AC210B92449F1DD1628497DAE7",
				16);
		BigInteger pubExp = new BigInteger("010001", 16);

		RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, pubExp);
		KeyFactory kf = KeyFactory.getInstance("RSA");
		PublicKey publicKey = kf.generatePublic(keySpec);

		Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		cipher.init(Cipher.ENCRYPT_MODE, publicKey);

		// byte[] decodedStr = Base64.decode(key, Base64.DEFAULT);
		byte[] plainText = cipher.doFinal(b1);

		return plainText;
	}
}

goo.gl/t27IWw
(Надо нажать Compile, Execute)

ik1Dvev7AffP+mOgxkbnYmpZrN9nGCKEzwCA4qsADcSKZFDYC/32B4uzUNSH8D+yCjBbrE5HUDL6vs6W5idG6Q==
Android программа

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;

import javax.crypto.Cipher;

import android.util.Base64;
import android.util.Log;

public class TestX {

	public static byte[] decrypt(String key) throws Exception {

		BigInteger modulus = new BigInteger(
				"BA9B1DE44B0B239DCA40B94832EA238F40AD81981B59AA687F7F1A75A319ABD9334D9FA2CE25736C3AB4272171404DB49BC6A8AC210B92449F1DD1628497DAE7",
				16);
		BigInteger exp = new BigInteger(
				"00a9fa3beb45143b7832d06c35c0f3183b180e341b0ae6cd5becaf29b9f83c9d1466811e6f09af1f5a2e29a52e69c25cb224242b34c795ebb892b6065063b59841",
				16);

		RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(modulus, exp);
		KeyFactory kf = KeyFactory.getInstance("RSA");
		PrivateKey privKey = kf.generatePrivate(keySpec);

		Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		cipher.init(Cipher.DECRYPT_MODE, privKey);

		byte[] decodedStr = Base64.decode(key, Base64.DEFAULT);
		byte[] plainText = cipher.doFinal(decodedStr);

		return plainText;
	}

	public static void test() {
		try {
			byte[] b1 = decrypt("JutBa0GLHzGrlygxwWr66cizw4W4za+DbzZweNM0iloCD7xEP9LclL013lcksJL5XhjW44U+oxpqncX1ZSLhWuA==");
			String s1 = new String(b1, "UTF-8");
			Log.i("TEST", s1);
			byte[] b2 = encrypt("Java kills".getBytes("UTF-8"));
			String s2 = Base64.encodeToString(b2, Base64.CRLF);
			Log.i("TEST", s2);
			byte[] b3 = decrypt(s2);
			String s3 = new String(b3, "UTF-8");
			Log.i("TEST", s3);

		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static byte[] encrypt(byte[] b1) throws Exception {
		BigInteger modulus = new BigInteger(
				"BA9B1DE44B0B239DCA40B94832EA238F40AD81981B59AA687F7F1A75A319ABD9334D9FA2CE25736C3AB4272171404DB49BC6A8AC210B92449F1DD1628497DAE7",
				16);
		BigInteger pubExp = new BigInteger("010001", 16);

		RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, pubExp);
		KeyFactory kf = KeyFactory.getInstance("RSA");
		PublicKey publicKey = kf.generatePublic(keySpec);

		Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		cipher.init(Cipher.ENCRYPT_MODE, publicKey);

		byte[] plainText = cipher.doFinal(b1);

		return plainText;
	}
}

КО: шифровать можно не текст, а AES ключ.

Всем весны и удачи.

Автор: pyra

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js