Android离线验证码

GITHUB

GITEE

一. 背景:

当客户端在某些情况下需要离线给用户授权,或某些功能需要提前植入(硬编码到 Apk 中),但这些功能又仅想部分设备可用。或部分设备单次/某时间段儿可用(P1 阶段仅提供部分设备可用能力)。又或者 Apk 公测体验时,测试版本只针对部分设备开放授权。但 Apk发布出去后,谁都可以安装,无法控制安装。又不想为了简单的授权逻辑搭建一套复杂/昂贵的后台服务器进行注册/认证。那么此时就需要有一套技术/工具,能够实现 Android Apk 在离线的时候,也能验证设备的合法性(P1 阶段)以及有效次数和有效时间(P2 阶段)

注:这里说的离线,是指 Apk 从安装使用开始就不需要调用任何网络接口,纯本地 Apk。

二. 原理:

使用非对称加密,利用Android Apk 在打包签名时的 keystore 中的 RSA 密钥对中的私钥,进行签名。再使用 Apk 从自身获取到应用签名的公钥进行验签。验签通过即验证成功!

在这过程中,需要解决签名原材料唯一的问题。本工具使用 Android 手机的 ANDOIRD_ID 作为客户端唯一标识(恢复出厂设置才变)并提供了适配接口,可由开发者自由指定唯一标识。比如 IMEI/SN 等(老版本需要有 READ_PHONE_STATUS 权限,新版本 A10 以后需要有 android.permission.READ_PRIVILEGED_PHONE_STATE 权限或 DeviceOwner / ProfileOwner 权限)或其他唯一标识(有厂家接口或 SDK能够生成唯一标识)。

有了签名原材料后,客户端就可以将签名原材料交给管理员(开发者),让开发者使用私钥对原材料进行签名。因私钥不外泄,所以理论上只有管理员(开发者)有签名权限。

之后客户端使用自身公钥进行验签(因为客户端 Apk 是使用管理员/开发者自己创建的 keystore 文件签名的,所以私钥在管理员/开发者手中)并校验原材料是否正确来达到验证的目的。

三. 客户端使用过程简介:

  1. 导入依赖
//1. 在根目录settings.gradle.kt或build.gradle.kt中增加jitpack.io maven库配置
dependencyResolutionManagement {
		repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
		repositories {
			mavenCentral()
			maven { url 'https://jitpack.io' }
		}
	}

//2. 在具体项目中依赖oflauth aar包
dependencies {
	        implementation 'com.github.wpvsyou:oflauth:v1.0.1'
	}
  1. 客户端通过 SDK 中 verify 接口来验证本地本地验证码是否正确
val offlineAuthentication: OfflineAuthentication by lazy {
    OfflineAuthentication.Builder().build()
}
val s: Status = offlineAuthentication.verify()
  1. 其中 Status 为状态枚举

No.

Status

含义

1

AUTHORIZE_CODE_EMPTY

没有认证码

2

ILLEGAL_AUTHORIZATION_CODE

本地存在非法的认证码,可能已过期(P2 阶段)或认证码错误

3

PASS

认证通过

  1. 客户端上先通过 SDK 工具获取到客户端的唯一标识id:
val offlineAuthentication: OfflineAuthentication by lazy {
    OfflineAuthentication.Builder().build()
}
val id:String = offlineAuthentication.identityNumber
  1. 将客户端上的 id 告知给管理员
  2. 管理员使用 id 生成json配置文件(文件可从项目目录中下载),再对 json 文件的 md5 值进行签名。签名后将签名值(string字符串)进行 base64 编码,并将编码值添加到 json 配置文件中signature字段的值中。(该过程可参考“四. Linux/Macos命令行中,使用 KeyStore 文件生成离线验证码”章节的描述)
  3. 最后将这个 json 配置文件的内容发给客户端用户验证。(推荐通过二维码传递)
//其中 result 为二维码解析后的 String 串。
val s = offlineAuthentication.verify(
    offlineAuthentication.parser.deserialization(result)
)

四. Linux/Macos 命令行中,使用 KeyStore 文件生成离线验证码:(通过Android应用生成授权码请看第五章)

  1. 先将使用 keytool 命令将 keystore 转成 p12 文件。其中 app_key 是给 app 做签名的 keystore 文件(可由 AndroidStudio 生成,也可由命令行手动创建),keystore.p12 是将要生成的 p12 文件,key0 是 app_key keystore 中 RSA 密钥对的别名,是在创建 keystore 文件时指定的。当然还有 keystore 的密码,别名下密钥对的密码,这些都是在生成 keystore,生成密钥对时配置的。
keytool -importkeystore -srckeystore app_key -destkeystore keystore.p12 -deststoretype PKCS12 -srcalias key0
  1. 再使用 openssl 工具,通过 p12 文件导出 RSA 密钥对中的私钥 private_key.pem
openssl pkcs12 -in keystore.p12 -out private_key.pem -nocerts
  1. 再打印之前准备好的 json 格式配置文件。(注意参数顺序不能错,因为 json 是一种无序的数据结构,若两端参数顺序对不齐,就会造成 md5 计算出来的结果不一致,导致认证失败!)demo.json 就是我本地根据设备 identityNumber (需要换成你自己设备的identityNumber)生成的 json 格式配置文件。
cat demo.json
{"authorizationDate":123456,"deadline":123456,"duration":123456,"identityNumber":"8ee0460b8d4731f9","signature":""}
    • 如果你使用 vi/vim 工具编辑 json 格式配置文件时,一定要确保结尾没有被编辑器自动增加结束符/换行符。否则你 PC 上 json 格式文件 bytes 会比 Android 客户端里的 json 串 bytes 多一个 0x0a ,导致 md5 不一致,进而认证失败!)
//可以通过 xxd 工具打印一下 PC 上 json 文件的 bytes 数据检查一下:
xxd -i demo.json
unsigned char demo_json[] = {
  0x7b, 0x22, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
  0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x31, 0x32, 0x33,
  0x34, 0x35, 0x36, 0x2c, 0x22, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e,
  0x65, 0x22, 0x3a, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x2c, 0x22, 0x64,
  0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x31, 0x32, 0x33,
  0x34, 0x35, 0x36, 0x2c, 0x22, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
  0x79, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x3a, 0x22, 0x38, 0x65,
  0x65, 0x30, 0x34, 0x36, 0x30, 0x62, 0x38, 0x64, 0x34, 0x37, 0x33, 0x31,
  0x66, 0x39, 0x22, 0x2c, 0x22, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75,
  0x72, 0x65, 0x22, 0x3a, 0x22, 0x22, 0x7d, 0x0a
};

并与 Android 端上的 json 串 bytes 数据比较一下:

通过对比发现,PC 上 json 格式配置文件比 Android 客户端上需要验证的 json 数据多了一个 0x0a。通 过 BAIDU 发现,这个0x0a是 vi/vim自动创建的换行符/结束符。可在~/.vimrc 配置文件中添加如下配置

set noendofline binary

来取消 vi/vim 编辑器行尾的换行符/结束符。

配置完 vi/vim 配置文件后,需要 source 立即生效

source ~/.vimrc

最后再通过 vi/vim 重新创建一个 json 配置文件。再通过 xxd 检查 json 文件的 bytes 数组发现和 Android 端 json 字符串的 bytes 数据一致了。

xxd -i demo.json
unsigned char demo_json[] = {
  0x7b, 0x22, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
  0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x31, 0x32, 0x33,
  0x34, 0x35, 0x36, 0x2c, 0x22, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e,
  0x65, 0x22, 0x3a, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x2c, 0x22, 0x64,
  0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x31, 0x32, 0x33,
  0x34, 0x35, 0x36, 0x2c, 0x22, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
  0x79, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x3a, 0x22, 0x38, 0x65,
  0x65, 0x30, 0x34, 0x36, 0x30, 0x62, 0x38, 0x64, 0x34, 0x37, 0x33, 0x31,
  0x66, 0x39, 0x22, 0x2c, 0x22, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75,
  0x72, 0x65, 0x22, 0x3a, 0x22, 0x22, 0x7d
};
unsigned int demo_json_len = 115;
  1. 再使用命令行中的 md5 工具计算当前 json 配置文件的 md5 值
cat demo.json| md5
ae944a1cb22c93bcd75504a5be18883d
  1. 再使用第二步导出的私钥 private_key.pem 对 md5 值:ae944a1cb22c93bcd75504a5be18883d 进行签名并获取签名文件signature.bin
echo -n "ae944a1cb22c93bcd75504a5be18883d" | openssl dgst -sha256 -sign private_key.pem -out signature.bin
  1. 获取签名值 base64
cat signature.bin| base64
JyJCIUc/jlKGCZvoenLqQa0yaXw1jtv60Vx0d3LjLFsI41gDEKtSbant5vvafzlHSGDVZbcW/hBLENW2UylGsOIo5ptO51ePUBxpT6wXpV3AFOyZdMlVq6lVwR0vtSm6ZahJ1jZqq8BY7cajIcjeTh8IFvZFcsJ5k86XEHE+yHQWfy6vcCgSnExIq0vR57DvED2iOUeJ7xFqWiBeEhxxihCYpUjMm3Dpl9KXJha76iLewbPrlgqP/VYSHi1FwvxosQmlCOZVr50xOsBWY4DOZcIyFeAQhRUnModyuS0M582XN+F+diU29yvVeaw3NDq1BEmnE52DGkI+0LABMbRrvA==
  1. 将 base64 值添加到 json 配置文件的 "signature" 字段值,并获得如下配置文件。
{
  "authorizationDate": 123456,
  "deadline": 123456,
  "duration": 123456,
  "identityNumber": "8ee0460b8d4731f9",
  "signature": "JyJCIUc/jlKGCZvoenLqQa0yaXw1jtv60Vx0d3LjLFsI41gDEKtSbant5vvafzlHSGDVZbcW/hBLENW2UylGsOIo5ptO51ePUBxpT6wXpV3AFOyZdMlVq6lVwR0vtSm6ZahJ1jZqq8BY7cajIcjeTh8IFvZFcsJ5k86XEHE+yHQWfy6vcCgSnExIq0vR57DvED2iOUeJ7xFqWiBeEhxxihCYpUjMm3Dpl9KXJha76iLewbPrlgqP/VYSHi1FwvxosQmlCOZVr50xOsBWY4DOZcIyFeAQhRUnModyuS0M582XN+F+diU29yvVeaw3NDq1BEmnE52DGkI+0LABMbRrvA=="
}
  1. 最后,再通过命令行 qr 工具,将 json 配置文件转变为二维码图片 AuthorizeCodeQrCode.png,方便客户端扫码验证。
cat demo.json | qr > AuthorizeCodeQrCode.png

五. 从客户端生成授权码二维码并交由客户端验证

  1. 先将PEM格式私钥转成DER格式。
openssl rsa -in private_key.pem -outform DER -out private_key.der
  1. 将 DER 格式的私钥 push 到手机中,并由程序加载至内存。
  2. 用需要授权码授权的客户端生成的 identityNumber 去组装一个 AuthorizeCodeBean 对象并计算其 md5 值
val b = AuthorizeCodeBean(id, -1, -1, "", -1)
val m = offlineAuthentication.md5(offlineAuthentication.parser.serialize(b))
  1. 通过 signString 接口,对 md5 进行签名
b.signature = offlineAuthentication.signString(m, privateKey).toString()
  1. 最后将 AuthorizeCodeBean 对象序列化后传递给授权码授权的客户端进行验证即可。

六. DEMO:

  1. 通过App演示授权码生成过程:

  1. 客户端验证授权码过程演示:

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值