SQLite 全端统一:一个契约,一种加密文件格式
[LOADING...]
最初的 com.codename1.db 实现将 SQLite 调用委托给各操作系统提供的数据库。我们知道这比 Codename One 的其他部分可移植性更差,但要修复它,意味着要在每个目标平台上亲自接管 SQLite。这看起来是个很深的兔子洞,所以我们转而提供了一些逃生通道。我们在 2018 年描述的可插拔 SpatiaLite 工作 就是其中之一。
在 Windows 和 Linux 上,Database.openOrCreate() 返回 null。JavaScript 依赖于 WebSQL,而 Chrome 已将其移除,Firefox 则从未实现过。iOS 游标可能在空结果集上报告成功,然后读取未设置的内存。模拟器从 1 开始对行计数,而其他端口从 0 开始。加密也没有合适的位置,因为底层没有一个统一的数据库契约。
PR #5526 一探到底。它用一份明确规范且经过测试的 SQLite 契约取代了依赖平台的实现,也解决了长期存在的加密数据库请求。同一个加密文件可以在不同端口之间移动,并能在标准 SQLCipher 4 客户端中打开。
快速概览
- SQLite 现在有一份统一且经过测试的契约,包含原生 Windows 和 Linux 实现、面向 JavaScript 的 WebAssembly SQLite,以及三种密钥模型的加密。
- 手表应用深入解析 将手机和手表视为具有各自生命周期的独立应用。
WearableConnection为二者提供一个异步 API,用于消息、复制状态和文件。 - JavaScript 文本 现在以真实 DOM 文本形式出现在 canvas 之上。浏览器搜索、选择、无障碍、自动填充和原生分辨率渲染都能工作,而无需把布局交给浏览器。
- 智能家居支持 将 HomeKit、Matter 和 Google Home 的概念映射到
com.codename1.home,并包含一个用于桌面开发的模拟房屋。 - 点击劫持防护 可以检测并拒绝在另一个应用覆盖层之后开始的 Android 手势。
- 相机和视觉 重新获得了在旧扫描仪库被更底层的设备端分析器取代时所失去的一次调用便利性。
- App Intents 将 Java 处理器暴露给 Siri、Spotlight、快捷指令(Shortcuts)、Android 启动器快捷方式以及一个内部命令层。
每个平台原本就有不同的 SQLite 行为
旧 API 看起来统一,因为每个端口都实现了相同的 Java 方法。但这并不能说明实际结果。
execute(sql) 在 iOS 上会执行每一条语句,但在模拟器中只执行第一条语句后就静默停止。getBlob() 在 iOS 上返回 nil。JavaScript 游标的 position(n) 总是落在第 0 行。模拟器每次查询都会泄漏一条预处理语句。ThreadSafeDatabase.close() 在关闭真正发生之前就返回,因此紧接着的删除操作可能与它发生竞争。
这些并不是围绕某个可用抽象层展开的边界情况。它们意味着在模拟器中测试通过的查询,在设备上可能会有不同的事务、游标或绑定行为。
新的 DatabaseConformanceSuite 只定义一份契约。7 项设备测试会在每个端口状态目标上验证生命周期、语句、游标、事务、加密和旧行为。当前报告显示这些测试在 Android、两种 iOS 渲染器、JavaScript、原生 Linux、原生 Windows、macOS、tvOS 和 watchOS 上均通过。
[LOADING...]
游标导航现在由两个原语 rewind() 和 stepForward() 派生,而不再在每个端口重新实现。事务在提交或回滚后都会回到自动提交模式。BLOB 和类型化参数的行为保持一致。Windows 和 Linux 终于能打开数据库,而不是返回 null。
修复这些不一致会改变一些现有应用可能依赖的行为。即使游标索引或事务怪异行为是偶然产生的,也可能被应用代码所依赖。db.legacy 构建提示可在迁移期间保留旧实现可用。
旧版 Ant 项目默认使用旧模式。它们可能只是需要另一个维护版本的稳定应用,强制让它们迁移数据库会过于鲁莽。较新的 Maven 项目默认使用修正后的契约,从而避免新代码继承旧问题。两种项目类型都可以设置 db.legacy=true 来使用旧行为,或设置 db.legacy=false 来使用修正后的契约。
DatabaseConfig 在任何地方打开相同的加密格式
打开数据库时传入 DatabaseConfig 即可选择加密:
有三种密钥模型:
编译进应用中的口令可以从发布二进制中恢复出来,因此并不是真正有用的机密。托管密钥会避免这个错误,但也改变了恢复方式:如果密钥库条目丢失,数据库将无法读取。当数据需要在设备丢失后仍然可用时,应使用由用户或服务器持有的口令。
现有明文数据库无需替换 API 即可转换:
磁盘上的参数固定为 SQLCipher 4 格式:AES-256-CBC、PBKDF2-HMAC-SHA512、256,000 次迭代、4096 字节页,以及每页 HMAC-SHA512。CI 会先用 Codename One 引擎写入数据库,再用标准 sqlcipher 客户端读取,然后反过来再做一次。这种跨引擎测试能捕获一类失败:每个端口都能读取自己的文件,但文件彼此不兼容。
加密保护的是静态数据。它不能保护已打开的数据库免受调试器、内存转储或已经在受损设备上运行的代码的攻击。正是这个边界,使这项工作与 App Shield、App Hardening 形成互补,而不是取代其中任何一个。
JavaScript 用 SQLite 取代已被移除的浏览器 API
JavaScript 端口过去使用 WebSQL,因为浏览器曾把它作为一种方便的、类 SQL 的存储方式暴露出来。Chrome 在 119 版本中移除了 WebSQL,Firefox 则从未提供过。一个可移植的 API 不能依赖一个在一个浏览器中已不存在、在另一个浏览器中从未存在的功能。
JavaScript 现在运行编译为 WebAssembly 的 SQLite,并将数据库存储在浏览器的存储池中。它通过了与原生端口相同的语句、游标、事务、加密和兼容性测试。
新引擎无法读取旧的 WebSQL 存储。openOrCreate() 在检测到旧数据时会拒绝创建空数据库,因为用空状态静默替换用户数据库,比一次可见的迁移更糟。仍然使用 WebSQL 时代数据的应用,必须从仍能读取旧存储的构建版本中导出数据,或者通过设置 cn1.db.ignoreLegacyWebSql=true 明确选择创建一个新的空数据库。
构建也仍然遵循按需使用的原则。从不引用 com.codename1.db 的应用不会获得数据库引擎。未加密的 JavaScript 数据库大约会增加 1.5 MB 的 WebAssembly。引用 DatabaseConfig 则会在平台需要时加入加密实现。
在 Android 上,加密会将最低 SDK 提高到 23,并要求使用 AndroidX,因为这是 SQLCipher 的要求。未加密数据库路径仍保持之前的更低门槛。
以上是数据库工作全部内容。本周还发布了另外 6 项变更,每项都有各自的详细介绍文章。
手表和手机现在作为独立应用运行
手表并不是由手机进程拥有的一个小型第二窗口。它有自己独立的可执行文件、存储、启动顺序和故障模式。手表应用文章 介绍了 PR #5487 引入的完整模型。
一个 codename1.watchMain 设置即可添加 Apple Watch 配套入口点。在 Android 上,codename1.watchStandalone=true 会把该入口点构建为 Wear OS 产品,而不是手机应用;目前还不会在手机 APK 旁边生成配套的 Wear 构件。模拟器可以把手机和手表作为两个独立进程启动,并在桌面端将它们连接起来。两者共享源代码、资源、CSS 和 surfaces 模型,但不共享 Storage、Preferences 或 SQLite。
WearableConnection 对三种平台实际提供的传输方式建模。sendMessage() 向在线对端请求即时回复;putData() 复制最新状态,并在睡眠或重启后仍然保留;transferFile() 在后台传输较大的负载。该 API 在 Apple 平台使用 WCSession,在 Android 使用 Wearable Data Layer。
复杂功能(Complications)通过手表专用的 WidgetSize 家族加入现有 surfaces 词汇表。这复用了小组件和 Live Activities 已经使用的内容与时间线模型。生成的 watchOS 复杂功能目标和 Wear OS 复杂功能服务尚未包含在此版本中。Android 目前也支持独立 Wear 应用,但尚未在手机 APK 旁边生成配套的 Wear 构件。
浏览器搜索现在可在 JavaScript 端口使用
JavaScript 端口一直通过 canvas 渲染。这让 Codename One 能控制布局并保持 UI 一致,但同时也把可见文本变成了像素。浏览器搜索找不到它,用户无法选择它,浏览器也无法以原生分辨率栅格化它,或将其作为普通文本暴露出来。
JavaScript 文本文章 解释了 PR #5552 如何保留 canvas 渲染器,并在其上方增加两个有针对性的 DOM 层。文本层在 Codename One 已经计算好的位置上保存可见文本段;无障碍层保存 ARIA 投影,并增量更新它。
浏览器无需重新排列文本行,也无权决定组件位置。它拿到的文本已经过测量、换行和定位。页面内查找、文本选择、无障碍、原生输入元数据、密码管理器提示和高 DPI 渲染因此可以使用浏览器机制,而无需改变应用布局。
拖拽选择仍然关闭,因为文本层如果接收指针事件,就会改变 canvas 的命中测试。形状裁剪和变换后的文本仍留在 canvas 上。同一表单内的遮挡,例如一个 sheet 覆盖底层表单中的文本,也还需要进一步处理。
一个 API 同时覆盖 HomeKit、Matter 和 Google Home
智能家居文章 介绍了 PR #5554 引入的 com.codename1.home,这是面向 HomeKit、Matter 和 Google Home 的统一模型。配件包含服务,服务暴露规范性特征(canonical traits),例如 ON_OFF、BRIGHTNESS、TARGET_TEMPERATURE。平台标识符和单位约定保留在各自的端口中。
这听起来像一个普通的抽象层,直到不同平台之间出现分歧。Matter 和 HomeKit 对遮盖物位置的方向表达相反。Matter 在自动模式下没有单一的恒温器设定点。Android 可以用很少的设置将 Matter 配件加入 Google Home,但读取配件图谱需要 Google Home 开发者注册,而 Codename One 无法替你完成注册。
该 API 将该默认 Android 状态报告为 COMMISSIONING_ONLY,而不是 AVAILABLE。在 iOS 上,它可以读写 HomeKit 特征、运行场景,并接入 Matter 配件。模拟器、桌面端口和 JavaScript 会运行一个故意设计得有些别扭的合成房屋,以便在没有硬件的情况下测试缺失值、部分失败和轮询行为。
Android 上的自动化、后台配件事件、摄像头流、报警面板和 Google Home 配件图谱不在本版本范围内。每个缺口都会有相应的能力查询或具体的可用性状态,而不是变成一个看起来像真实答案的空列表。
点击劫持防护继续推进安全加固工作
我们正在继续推动 Codename One 成为应用开发者默认的安全选择。点击劫持防护文章 介绍了这项工作在输入侧的成果。点击劫持(tapjacking)发生在另一个 Android 应用绘制在敏感屏幕之上,从而改变用户对点击操作后果的认知时。PR #5553 为 DeviceIntegrity 增加了 TapjackingPolicy。
REPORT 只观察,不改变输入。BLOCK 会丢弃在完全被遮挡时开始的手势。STRICT 还会丢弃部分被遮挡的手势,这可能包括普通系统 UI,因此会带来真实的误报成本。
检测由触摸驱动,因为 Android 会在 MotionEvent 中提供遮挡状态。没有伴随触摸事件的覆盖层不会通过轮询检测到。在 Android 12 及更高版本上,setHideOverlayWindows(true) 会阻止覆盖窗口出现,而不是在触摸时对其作出反应。iOS 不允许一个应用绘制在另一个应用之上,因此该策略是 Android 专属控制项,而不是一个虚假的跨平台开关。
无需重建相机管线即可扫描条码
设备端视觉工作增加了条码识别、人脸与姿态检测、文本识别、分割、文档扫描和图像标注。它还把从相机到分析器的每一段管线都暴露了出来。这对自定义相机产品很有用,但对常见场景来说则是不必要地底层。
相机与视觉文章 介绍了 PR #5575 中的更高级 API。CodeScanner.scan() 拥有一个完整的扫码界面,并返回一个异步结果:
VisionCameraView 将同一管线封装为一个组件,供你自己拥有的表单使用。类型化的 BarcodeFormat、FaceLandmarks 和 PoseLandmarks 常量取代了字符串字面量。VisionRect.toBounds(...) 和 VisionPoint.toPoint(...) 将归一化的分析器几何数据转换为组件坐标。SegmentationMask.cutOut(...) 将自拍人像分割转换为可用的图像操作。
预览仍然是一个原生 peer(native peer)。组件无法在每个目标平台上都统一绘制在它之上,因此控件和瞄准框应放置在预览周围。同一时间只能打开一个相机会话。模拟器现在可以脚本化视觉结果,从而让流程、覆盖层几何、取消和错误路径在应用运行到设备之前就能得到验证。## 向 Siri、Spotlight 和快捷指令暴露意图
App Intents 文章展示了如何将 Java 方法暴露给 Siri、Spotlight、快捷指令或 Android 启动器快捷方式。PR #5559 将该声明写入 Java:
Maven 插件会读取编译后的字节码,并生成一份无需反射的调度表以及原生声明。直接静态调用在 iOS 上尤为重要,因为运行时注解查询不可用,而死代码消除可能会移除没有 Java 调用方的处理器。
实体(Entities)让平台可以向用户询问他们指的是哪个应用对象。Intents.index(...) 会将这些对象发布到设备搜索中。当结果需要让某个屏幕进入前台时,opensRoute 会将意图连接到现有的路由表。
Android 并未被描述为与 Siri 对等。它获得的是启动器快捷方式、捐赠(donation)、索引(indexing)和后台执行。语音调用、系统消歧和语音助手结果都属于 iOS 的能力。即使操作系统没有暴露任何意图表面,Intents.invoke(...) 仍然作为内部命令层在每个端口上工作。
安全现已涵盖存储、代码、后端信任与输入
本周填补了多个曾被解释为平台差异的缺口。SQLite 现在拥有一个文档化的契约和一种可移植的加密格式。手表和手机是带有明确通道的独立应用。智能家居特征(Smart-home traits)和应用意图(App Intents)将同一个应用模型投射到原生系统服务上,而不假装这些服务是相同的。Web 端口保留了我们的渲染器,同时恢复了用户期望的浏览器行为。
App Shield 将信任决策转移到后端。App Hardening 提高了读取和修改已发布二进制文件的成本。加密的 SQLite 保护存储的数据。点击劫持(Tapjacking)保护可以拒绝 Android 上的一类误导性输入。这些控制措施都无法让一台已被攻陷的设备变得可信。但它们共同覆盖了从存储字节到授权操作的那次点击之间更多的路径。
我们希望 Codename One 在安全方面引领跨平台开发。这不仅仅需要添加孤立的安全开关。安全路径必须能从普通应用代码中获取、仅在用到时才被包含,并且在我们声称支持的每个目标平台上都经过测试。本周的加密数据库和点击劫持防护工作,将另外两个边界移入了这条默认路径。
如果你有需要迁移的静态数据,请从数据库指南开始。现有的 Ant 应用在迁移到 Maven 之前,应显式设置 db.legacy。使用 WebSQL 时代浏览器数据的应用,在采用新的 JavaScript 引擎之前需要制定导出计划。
该文章 SQLite Across Every Port: One Contract, One Encrypted File Format 最初发布在 foojay 上。