1. 网络通讯的安全性问题及解决方案
在基于TCP或者UDP的通讯中,通讯内容是明文发送和接收的,对于安全性要求不太高的通讯场景,这种方式因为实现简单,传输效率高而得到广泛应用;
但是,如果数据包在传输过程中被拦截,攻击者可以直接读取其中的信息,这使得用户的敏感信息(如密码、个人资料等)容易遭受窃听或篡改。要避免这种情况的发生,
就需要对传输过程进行加密,典型的是基于TLS协议的通讯,它通过加密技术确保数据的保密性和完整性,防止数据在传输过程中被窃听或篡改。当使用TLS进行通讯时,客户端和服务器先进行握手,在这个过程中双方协商加密算法、交换加密密钥等,之后所有传输的数据都会被加密,即使数据包被第三方截获,由于没有解密密钥,第三方也无法读取数据的真实内容。
本系列的第33篇文章,在API 12环境下使用ArkTS语言实现了TLS回声服务器,本篇文章将在API 17环境下,使用仓颉语言实现TLS回声服务器。
在目前的版本里,鸿蒙并没有提供TLS服务端相关的API,所以,本文将使用仓颉语言原生的TLS相关API实现,典型的类有TlsSocket、TlsServerConfig等,它们在net.tls包里。
TLS服务端的实现还需要服务端数字证书及私钥,需要预先准备好相关的文件并上传到鸿蒙设备中。
2. TLS回声服务器演示
本示例运行后的界面如图所示:
选择服务端数字证书及服务端数字证书私钥,然后单击“启动”按钮,可以绑定服务端到本地端口,如图所示:
更进一步的TLS通讯需要TLS客户端的配合,我们将在下一篇文章介绍TLS服务端和客户端的数据收发过程。
3. TLS回声服务器示例编写
下面详细介绍创建该示例的步骤(确保DevEco Studio已安装仓颉插件)。
步骤1:创建[Cangjie]Empty Ability项目。
步骤2:在module.json5配置文件加上对权限的声明:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
这里添加了访问互联网的权限。
步骤3:在build-profile.json5配置文件加上仓颉编译架构:
"cangjieOptions": {
"path": "./src/main/cangjie/cjpm.toml",
"abiFilters": ["arm64-v8a", "x86_64"]
}
步骤4:在main_ability.cj文件里添加如下的代码:
package ohos_app_cangjie_entry
internal import ohos.base.AppLog
internal import ohos.ability.AbilityStage
internal import ohos.ability.LaunchReason
internal import cj_res_entry.app
import ohos.ability.*
//Ability全局上下文
var globalAbilityContext: Option<AbilityContext> = Option<AbilityContext>.None
class MainAbility <: Ability {
public init() {
super()
registerSelf()
}
public override func onCreate(want: Want, launchParam: LaunchParam): Unit {
AppLog.info("MainAbility OnCreated.${want.abilityName}")
globalAbilityContext = Option<AbilityContext>.Some(this.context)
match (launchParam.launchReason) {
case LaunchReason.START_ABILITY => AppLog.info("START_ABILITY")
case _ => ()
}
}
public override func onWindowStageCreate(windowStage: WindowStage): Unit {
AppLog.info("MainAbility onWindowStageCreate.")
windowStage.loadContent("EntryView")
}
}
步骤5:在index.cj文件里添加如下的代码:
package ohos_app_cangjie_entry
import ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import ohos.file_picker.*
import ohos.ability.*
import ohos.file_fs.*
import crypto.x509.*
import ohos.crypto.*
import std.convert.*
import net.tls.*
import std.socket.*
@Observed
//文件选择状态
class FileSelectStatus {
@Publish
public var fileSelected: Bool = false
@Publish
public var fileUri: String = ""
}
@Entry
@Component
class EntryView {
@State
var title: String = 'TLS回声服务器示例';
//连接、通讯历史记录
@State
var msgHistory: String = ''
//证书文件选择状态
@State
var certFileStatus: FileSelectStatus = FileSelectStatus()
//私钥文件选择状态
@State
var keyFileStatus: FileSelectStatus = FileSelectStatus()
//服务端端口
@State
var port: UInt16 = 9990
//服务运行状态
@State
var running: Bool = false
//服务端套接字
var echoServer: ?TcpServerSocket = None
let scroller: Scroller = Scroller()
func build() {
Row {
Column {
Text(title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.width(100.percent)
.textAlign(TextAlign.Center)
.padding(10)
Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {
Text("服务端数字证书:").fontSize(14).width(90).flexGrow(1)
Button("选择").onClick {
evt => selectFile(this.certFileStatus)
}.width(60).fontSize(14)
}.width(100.percent).padding(5)
Text(certFileStatus.fileUri).fontSize(14).width(100.percent).padding(10)
Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {
Text("服务端数字证书私钥:").fontSize(14).width(90).flexGrow(1)
Button("选择").onClick {
evt => selectFile(this.keyFileStatus)
}.width(60).fontSize(14)
}.width(100.percent).padding(5)
Text(keyFileStatus.fileUri).fontSize(14).width(100.percent).padding(10)
Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {
Text("绑定的服务器端口:").fontSize(14).width(90).flexGrow(1)
TextInput(text: port.toString())
.onChange({
value => if (value == "") {
port = 0
} else {
port = UInt16.parse(value)
}
})
.setType(InputType.Number)
.width(80)
.fontSize(11)
Button(if (running) {
"停止"
} else {
"启动"
})
.onClick {
evt => if (!running) {
startServer()
} else {
stopServer()
}
}
.width(60)
.fontSize(14)
.enabled(certFileStatus.fileSelected && keyFileStatus.fileSelected && port != 0)
}.width(100.percent).padding(5)
Scroll(scroller) {
Text(msgHistory)
.textAlign(TextAlign.Start)
.padding(10)
.width(100.percent)
.backgroundColor(0xeeeeee)
}
.align(Alignment.Top)
.backgroundColor(0xeeeeee)
.height(300)
.flexGrow(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.On)
.scrollBarWidth(20)
}.width(100.percent).height(100.percent)
}.height(100.percent)
}
//选择文件
func selectFile(fileSelectStatus: FileSelectStatus) {
let picker = DocumentViewPicker(getContext())
let documentSelectCallback = {
errorCode: Option<AsyncError>, data: Option<Array<String>> => match (errorCode) {
case Some(e) => msgHistory += "选择失败,错误码:${e.code}\r\n"
case _ => match (data) {
case Some(value) =>
fileSelectStatus.fileUri = value[0]
fileSelectStatus.fileSelected = true
case _ => ()
}
}
}
picker.select(documentSelectCallback, option: DocumentSelectOptions(selectMode: DocumentSelectMode.MIXED))
}
//启动tls服务器
func startServer() {
let socketAddress = SocketAddress("0.0.0.0", port)
//回显TcpSocket服务端
echoServer = TcpServerSocket(bindAt: socketAddress)
let tlsCfg = getTlsServerCfg()
//允许恢复tls会话
let sessionContext = TlsSessionContext.fromName("echo-server")
//启动一个线程监听客户端连接
spawn {
//绑定到本地端口
echoServer?.bind()
msgHistory += "绑定到本地端口,等待连接...\r\n"
running = true
while (true) {
//已接受客户端连接
let acceptEchoSocket = echoServer?.accept()
if (let Some(echoSocket) <- acceptEchoSocket) {
msgHistory += "接受新的连接,对端地址为:${echoSocket.remoteAddress.kapString()}\r\n"
//启动一个线程处理新的socket
spawn {
try {
//生成服务端TLS套接字
let tlsSocket = TlsSocket.server(echoSocket, sessionContext: sessionContext,
serverConfig: tlsCfg)
//握手
tlsSocket.handshake()
msgHistory += "已握手\r\n"
//处理加密通讯
dealWithEchoSocket(tlsSocket)
} catch (err: SocketException) {
println(err.message)
}
}
}
}
}
}
//从socket读取数据并回写到socket
func dealWithEchoSocket(echoSocket: TlsSocket) {
//存放从socket读取数据的缓冲区
let buffer = Array<UInt8>(1024, item: 0)
while (true) {
//从socket读取数据
var readCount = echoSocket.read(buffer)
if (readCount > 0) {
//把接收到的数据转换为字符串
let content = String.fromUtf8(buffer[0..readCount])
msgHistory += "[${echoSocket.remoteAddress}]:${content}\r\n"
//回写客户端,把content写入echoSocket
echoSocket.write(content.toArray())
}
}
}
//停止tls服务器
func stopServer() {
echoServer?.close()
running = false
msgHistory += "服务已停止\r\n"
}
//获取服务端TLS配置信息
func getTlsServerCfg() {
//得到服务端x509数字证书
let x509 = getCert(certFileStatus.fileUri)
//得到服务端私钥
let privateKey = getPrivateKey(keyFileStatus.fileUri)
var tlsCfg = TlsServerConfig(x509, privateKey)
//设置支持的TLS版本
tlsCfg.maxVersion = TlsVersion.V1_3
tlsCfg.minVersion = TlsVersion.V1_2
return tlsCfg
}
//获取私钥
func getPrivateKey(keyPath: String) {
//获取文件在沙箱cache文件夹的路径
let sandboxFilePath = getSandboxFilePath(keyPath)
//从沙箱读取证书文件信息
let certContent = FileFs.readText(sandboxFilePath)
return PrivateKey.decodeFromPem(certContent)
}
//把文件复制到沙箱并返回沙箱中的文件路径
func getSandboxFilePath(oriFilePath: String) {
let fileName = getFileNameFromPath(oriFilePath)
let file = FileFs.open(oriFilePath)
//构造文件在沙箱cache文件夹的路径
let sandboxFilePath = getContext().filesDir.replace("files", "cache") + "/" + fileName
//复制私钥到沙箱给定路径
FileFs.copyFile(file.fd, sandboxFilePath)
//关闭文件
FileFs.close(file)
return sandboxFilePath
}
//获取数字证书
func getCert(certPath: String) {
//获取文件在沙箱cache文件夹的路径
let sandboxFilePath = getSandboxFilePath(certPath)
//从沙箱读取证书文件信息
let certContent = FileFs.readText(sandboxFilePath)
return X509Certificate.decodeFromPem(certContent)
}
//从文件路径获取文件名称
public func getFileNameFromPath(filePath: String) {
let segments = filePath.split('/')
//文件名称
return segments[segments.size - 1]
}
//获取Ability上下文
func getContext(): AbilityContext {
match (globalAbilityContext) {
case Some(context) => context
case _ => throw Exception("获取全局Ability上下文异常")
}
}
}
步骤6:编译运行,可以使用模拟器或者真机。
步骤7:按照本文第2部分“TLS回声服务器演示”操作即可。
4. 代码分析
仓颉语言版本的TLS服务器和ArkTS版本的实现差异非常大,基本没有相似性,相对来说,仓颉语言版本更靠底层一些,首先是启动一个TCP服务器,在指定的端口进行监听,在监听到新的客户端连接时,启动一个新的线程处理该连接,该线程调用TlsSocket的server函数生成一个服务端TLS套接字,接着处理该套接字的读写,主线程则继续监听新的连接。
(本文作者原创,除非明确授权禁止转载)
本文源码地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tls/TLSEchoServer4Cj