注册登录请求中RSA加密,PHP服务器和Android客户端实现

来源:互联网 发布:51单片机蜂鸣器程序 编辑:程序博客网 时间:2024/05/22 16:43

前言

客户端利用Http协议进行注册和登录等操作时,如果不做特殊处理,请求中携带的密码等敏感信息是明文传输的,有可能会被截获。解决这个问题最好的方法当然是使用Https协议,但是Https协议需要像权威机构申请证书才能保证足够的安全性,在没有证书的情况下,可以考虑自己来实现加密解密处理。

我们现在的场景只考虑在Http请求中加密,Http响应中没有敏感信息,暂时不考虑加密。首先考虑下对称加密的方式,这种方式客户端和服务器保存了一份相同的密钥,客户端加密和服务器解密是都需要用到,但是客户端程序有被破解的可能性,密钥有可能被泄露,攻击者拿到密钥再去截获请求报文,就可以解密出敏感信息来。因此最后选择了非对称加密RSA算法,客户端使用公钥加密,服务端再利用私钥解密,这样客户端只需要保存公钥了,即使泄露了也没法用来加密。可以实现敏感信息和密钥都处于相对安全的状态。
Github地址:
服务器:https://github.com/zhongchenyu/jokes-laravel
Android: https://github.com/zhongchenyu/jokes

PHP服务端实现

1. 生成密钥

可以利用 openssl 命令来生成RSA的密钥,一般linux系统都预装了。另外项目用的是 Laravel 框架,可以利用框架生成命令,将创建密钥的命令封装起来。
首先在项目路径下执行命令 php artisan make:command GenerateRSAKey
然后编辑GenerateRSAKey类的handle函数:

 public function handle()    {        //      $keyDir = 'sec';      if(!is_dir($keyDir)) mkdir($keyDir);      echo getcwd() . "\n";      chdir($keyDir);      echo getcwd() . "\n";      //生成原始 RSA私钥文件 rsa_private_key.pem      shell_exec('openssl genrsa -out rsa_private_key.pem 1024');      //将原始 RSA私钥转换为 pkcs8格式      shell_exec('openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out private_key.pem');      //生成RSA公钥 rsa_public_key.pem      shell_exec('openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem');      chdir('..');      echo getcwd() . "\n";      return;    }

执行命令后就会在项目的 sec 目录下生成公钥和私钥文件了。

2. 加密解密方法封装

创建一个RSAUtils类来专门处理加密和解密,实际上服务器只需要利用私钥解密就够了,不过为了以后的可扩展性,把所有的加解密方法都先写了:

class RsaUtils {  public static function enPublic($data)  {    $path = base_path();    $publicKey = openssl_get_publickey(file_get_contents($path.'/sec/rsa_public_key.pem'));    openssl_public_encrypt($data,$encrypted,$publicKey);    $base64Encoded = base64_encode($encrypted);    return $base64Encoded;  }  public static function dePrivate($data)  {    $path = base_path();    $privateKey = openssl_get_privatekey(file_get_contents($path.'/sec/rsa_private_key.pem'));    openssl_private_decrypt(base64_decode($data), $decrypted, $privateKey);    return $decrypted;  }  public static function enPrivate($data) {    $path = base_path();    $privateKey = openssl_get_privatekey(file_get_contents($path.'/sec/rsa_private_key.pem'));    openssl_private_encrypt($data, $encrypted, $privateKey);    $base64Encoded = base64_encode($encrypted);    return $base64Encoded;  }  public static function dePublic($data) {    $path = base_path();    $publicKey = openssl_get_publickey(file_get_contents($path.'/sec/rsa_public_key.pem'));    openssl_public_decrypt(base64_decode($data), $decrypted, $publicKey);    return $decrypted;  }}

以私钥解密的方法dePrivate()来说明下,其余的类似。

$path = base_path();$privateKey = openssl_get_privatekey(file_get_contents($path.'/sec/rsa_private_key.pem'));

首先通过 file_get_contents() 读取密钥文件的内容,这里要注意PHP 的 file_get_contents()参数只能是绝对路径,所以先要获取项目路径,补充到密钥相对路径前面。
openssl_get_privatekey()则从字符串中获取密钥,如果密钥格式不合法,则会出错。

openssl_private_decrypt(base64_decode($data), $decrypted, $privateKey);

考虑网络中的数据经过base64编码的情况,这里首先对$data 进行base64解密,客户端也要配合进行base64编码。
然后用openssl_private_decrypt() 函数将base64解密后的$data, 利用 $privateKey 进行RSA私钥解密,解密后的数据存在$decrypted 中并返回。

3. 测试加解密接口

为了验证下加密解密方法是否可行,写了一个测试用的接口,对请求数据加密再解密:

public function rsaTest(Request $request)  {    $data = $request->get('data','abcdef');    echo 'base64:'.base64_encode($data).'<br>';    echo $data . '<br>';    echo 'encrypted by public key<br>';    $encrypted = RsaUtils::enPublic($data);    echo $encrypted . '<br>';    $decrypted = RsaUtils::dePrivate($encrypted);    echo $decrypted . '<br>';    echo 'encrypted by private key<br>';    $encrypted = RsaUtils::enPrivate($data);    echo $encrypted . '<br>';    $decrypted = RsaUtils::dePublic($encrypted);    echo $decrypted . '<br>';  }

访问结果,可以看出加密解密没有问题:

base64:YWJjZGVmabcdefencrypted by public keyVnW8RNMCoqVzDUuvjwwmNAv4MX5kpbIvCgaPdQNE+xeJO4wBaD7PHpMKv/Ts7ry8ANg3uDnh10owalPQLy4d8RcX3g30z/Npf3RB1zFGtKB06YlN/O6zWgFkBr6X1EBubc7xmsoAeXBPn06/pjBn3xiYHWYlj7U5V7chaC0gh6k=abcdefencrypted by private keyb0KVZcXPOWaZtLpN5MAjV87ikMynEV4j96aehypzJVwSZUTf+gIY4zQf4/KJnxPxleK4sHOKXhiaQ/QdrsSA1tmnKv8YMW7s1iAlreYB9KjfVgNI2agPhYA5pISXPpUv3BByo9wNWc2Ef5/5w6/Hzs30jrp1wwgCbZcu/jkPf8Q=abcdef

4. 加密的注册和登录接口:

只对密码进行加密处理。
注册接口:

public function encryptedRegister(Request $request)  {    $this->validator($request->all())->validate();    $params = $request->all();    $password = RsaUtils::dePrivate($params['password']);    if($password == null) {      return response()->json(['message' => 'Encryption error'], 400);    }    $params['password'] = $password;    $user  = $this->create($params);    $token = JWTAuth::fromUser($user);    return ["token" => $token];  }

登录接口:

public function encryptedAuthenticate(Request $request)  {    $credentials['email'] = $request->get('email');    $credentials['password'] = RsaUtils::dePrivate( $request->get('password'));    try {      // attempt to verify the credentials and create a token for the user      if (! $token = JWTAuth::attempt($credentials)) {        return response()->json(['error' => 'invalid_credentials'], 401);      }    } catch (JWTException $e) {      // something went wrong whilst attempting to encode the token      return response()->json(['error' => 'could_not_create_token'], 500);    }    $user = User::where('email', $credentials['email'])->first();    $userTransform = new UserTransformer();    return ['user'=> $userTransform->transform($user), 'token' => $token];  }

关键的一步,向将请求参数中的 password 进行解密:
$password = RsaUtils::dePrivate($params['password']);
解密之后的处理和没有加密机制时基本一致。

注册时要注意增加判断,如果$password 没有经过正确的加密,这是解密返回的是null,必须返回错误:

if($password == null) {      return response()->json(['message' => 'Encryption error'], 400);    }

否则可能会创建一个密码为 null 的用户,以后凡是用任何一未正确加密的密码都可以登录了。

5. 接口测试用例

因为项目托管在 Github 上,可以对接 Travis 进行自动化测试。
首先编辑.travis.yml 增加一条生成密钥的命令- php artisan rsa:generate

language: phpphp:  - 7.1service:  - mysqlbefore_script:  - composer install  - composer dump-autoload  - cp .env.travis .env  - php artisan jwt:generate  - php artisan key:generate  - php artisan vendor:publish  - mysql -e 'CREATE DATABASE IF NOT EXISTS jokes ;'  - php artisan migrate  - php artisan rsa:generatescript: phpunit

测试用例:

class EncryptedLoginTest extends TestCase {  use DatabaseTransactions;  public function testEncryptedLogin()  {    $user              = User::create([      'name'     => 'TestUser12345',      'email'    => 'TestUser12345@TestUser.com',      'password' => bcrypt('123456'),    ]);    $userId            = $user->id;    $encryptedPassword = RsaUtils::enPublic('123456');    $response          = $this->json('POST', '/api/encrypted_login', [      'name'     => 'TestUser12345',      'email'    => 'TestUser12345@TestUser.com',      'password' => $encryptedPassword    ]);    $response->assertStatus(200)->assertJsonStructure(['token'])    ->assertJson(['user' => ['id'=>$userId, 'name'=>'TestUser12345', 'email' =>'TestUser12345@TestUser.com']]);  }}

这样执行 git push 到Github后将自动执行环境部署和测试,通过:

$ phpunitPHPUnit 6.4.4 by Sebastian Bergmann and contributors.............                                                      12 / 12 (100%)Time: 3.08 seconds, Memory: 28.00MBOK (12 tests, 22 assertions)Generating code coverage report in Clover XML format ... doneThe command "phpunit" exited with 0.Done. Your build exited with 0.

服务器搞定,下面开始客户端编码。

Android 客户端

1. 加解密工具代码

密钥获取:

private static PublicKey getPublicKey(String publicKeyString) throws Exception{    byte[] keyBytes = Base64.decode(publicKeyString.getBytes(), Base64.DEFAULT);    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);    KeyFactory keyFactory = KeyFactory.getInstance("RSA");    return keyFactory.generatePublic(keySpec);  }  private static PrivateKey getPrivateKey(String privateKey) throws Exception {    byte[] keyBytes = Base64.decode(privateKey.getBytes(), Base64.DEFAULT);    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);    KeyFactory keyFactory = KeyFactory.getInstance("RSA");    return keyFactory.generatePrivate(keySpec);  }

加密和解密:

private static byte[] encrypted(byte[] content, PublicKey publicKey) throws Exception {    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");    cipher.init(Cipher.ENCRYPT_MODE, publicKey);    return cipher.doFinal(content);  }  private static byte[] decrypt(byte[] content, PrivateKey privateKey) throws Exception{    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");    cipher.init(Cipher.DECRYPT_MODE, privateKey);    return cipher.doFinal(content);  }

最终要用到的公开接口,利用公钥加密,并进行base64编码:

public static String base64Encrypted(String data)  {    byte[] encryptedBytes = {};    try {      PublicKey publicKey=getPublicKey(publicKeyString);      encryptedBytes = encrypted(data.getBytes(), publicKey);    } catch (Exception e) {      e.printStackTrace();    }    return Base64.encodeToString(encryptedBytes, Base64.NO_WRAP);  }

这里讲下中间遇到的几个坑,要注意下。客户端和服务器,一个加密一个解密,要能对接起来,除了要求用相同的算法,配套的公钥密钥,编码方式等,还要注意算法和编码中用到的一些细节参数,否则可能出现客户端加密,发送到服务端无法解密的情况。主要有这些:
Base64编码换行的问题
PHP里用base64_decode($data)base64_encode($data) 进行Base64编解码,不需要额外参数。
Java中可以使用 java.util.Base64 库的 Base64.getEncoder().encode(data)Base64.getDecoder().decode(data) 进行编解码,也不需要额外参数,并且是可以和PHP对接的,即一方编码,另一方解密没有问题。
Android虽然用的是Java 语言,但是却没有java.util.Base64 库,但是有专门的 android.util.Base64 库,使用函数 Base64.encode(byte[] bytes, int flag),有一个参数flag,使用默认的flag:Base64.DEFAULT 是无法和PHP的base64对接的,必须用Base64.NO_WRAP,因为 Android 的默认方式,会在编码过长是添加换行符,而PHP和Java则不会,因此需要使用Base64.NO_WRAP。

RSA分组和填充方式
PHP中用 openssl_private_decrypt($data, $decrypted, $privateKey) 不需要额外参数, Java中RSA 默认就是RSA/ECB/PKCS1Padding,但Android的话虽然用的和Java是一个库,但是必须制定RSA/ECB/PKCS1Padding

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");

其中RSA是加密算法,RSA是块算法,可以有不同的分组方式,ECB是其中最简单的一种,PKCS1Padding则是填充方式,这里需要指定。

2. 请求处理

public void register(String name, String email, String password) {    mName = name;    mEmail = email;    mPassword = RSAUtil.base64Encrypted(password);    start(REGISTER);  }  public void login(String email, String password) {    mEmail = email;    mPassword = RSAUtil.base64Encrypted(password);    start(LOGIN);  }

发送注册和登录前先将password加密,其余处理和不加密一样。

看下登录log,password被加密了:

11-26 17:04:21.891 4103-4185/chenyu.jokes D/OkHttp: email=zcy.gr%40qq.com&password=yJppgVBAbnG6yt8gIcTAKQ%2FRP7MdBU4Pg21b8V9KIys%2B6c8mJAUSAsBDu6bjqKLG****************
阅读全文
0 0