4.9 Token存储实现:PIN与元数据的安全存储
Token存储的职责
Token存储负责管理Token的核心元数据:
Token元数据内容:
Token元数据
│
├── 基本信息配置
│ ├── Label(Token名称)
│ ├── Serial(序列号)
│ └── Flags(Token状态标志)
│
├── 安全凭证存储
│ ├── SO PIN(安全管理员PIN)
│ └── User PIN(用户PIN)
│
└── 对象管理接口
├── 创建Object
├── 删除Object
├── 获取Object列表
└── 清空Token
ObjectStoreToken:抽象基类
ObjectStoreToken定义了Token存储的统一接口:
class ObjectStoreToken
{
public:
static bool selectBackend(const std::string& backend);
static ObjectStoreToken* createToken(const std::string basePath,
const std::string tokenDir,
int umask,
const ByteString& label,
const ByteString& serial);
static ObjectStoreToken* accessToken(const std::string &basePath,
const std::string &tokenDir,
int umask);
virtual bool setSOPIN(const ByteString& soPINBlob) = 0;
virtual bool getSOPIN(ByteString& soPINBlob) = 0;
virtual bool setUserPIN(ByteString userPINBlob) = 0;
virtual bool getUserPIN(ByteString& userPINBlob) = 0;
virtual bool getTokenFlags(CK_ULONG& flags) = 0;
virtual bool setTokenFlags(const CK_ULONG flags) = 0;
virtual bool getTokenLabel(ByteString& label) = 0;
virtual bool getTokenSerial(ByteString& serial) = 0;
virtual std::set<OSObject*> getObjects() = 0;
virtual OSObject* createObject() = 0;
virtual bool deleteObject(OSObject* object) = 0;
virtual bool isValid() = 0;
virtual void invalidate() = 0;
virtual bool clearToken() = 0;
virtual bool resetToken(const ByteString& label) = 0;
virtual ~ObjectStoreToken() {};
};
两种实现:
| 实现 | 存储方式 | 适用场景 |
|---|---|---|
| OSToken | 文件系统 | 默认,简单易用 |
| DBToken | SQLite数据库 | 可选,性能更好 |
后端选择机制
SoftHSM2支持两种存储后端:
static std::string backendType;
bool ObjectStoreToken::selectBackend(const std::string& backend)
{
backendType = backend;
return true;
}
ObjectStoreToken* ObjectStoreToken::createToken(...)
{
if (backendType == "db") {
return DBToken::createToken(basePath, tokenDir, umask, label, serial);
} else {
return OSToken::createToken(basePath, tokenDir, umask, label, serial);
}
}
配置方式:
在softhsm2.conf中配置:
# softhsm2.conf 示例
directories.tokendir = /var/lib/softhsm2/tokens
objectstore.backend = db # 使用数据库后端
# objectstore.backend = file # 使用文件后端(默认)
OSToken:文件存储实现
OSToken使用文件系统存储Token元数据:
Token目录结构:
/var/lib/softhsm2/tokens/
│
├── 1234567890abcdef/ ← Token目录
│ │
│ ├── token.object ← Token元数据文件
│ │ ├── CKA_LABEL
│ │ ├── CKA_SERIAL_NUMBER
│ │ ├── CKA_TOKEN_FLAGS
│ │ ├── SO_PIN_BLOB
│ │ └── USER_PIN_BLOB
│ │
│ ├── generation ← 版本文件
│ │
│ ├── xxxxxxxx.object ← Object文件
│ ├── ...
│ │
│ └── xxxxxxxx.lock ← Object锁文件
token.object文件:
Token元数据存储在一个特殊的ObjectFile中:
class OSToken : public ObjectStoreToken
{
private:
ObjectFile* tokenObject; // Token元数据存储在这里
};
PIN存储格式:
PIN不是明文存储,而是以“Blob“形式存储:
bool OSToken::setSOPIN(const ByteString& soPINBlob)
{
// soPINBlob是加密后的PIN数据
return tokenObject->setAttribute(CKA_SO_PIN, OSAttribute(soPINBlob));
}
bool OSToken::getSOPIN(ByteString& soPINBlob)
{
OSAttribute attr = tokenObject->getAttribute(CKA_SO_PIN);
soPINBlob = attr.getByteStringValue();
return true;
}
PIN Blob的结构
PIN Blob包含加密后的PIN和验证信息:
PIN Blob结构:
┌─────────────────────────────────────────────────────────────┐
│ PIN Blob │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 加密的PIN数据 │ │
│ │ │ │
│ │ ├── 加密算法标识 │ │
│ │ ├── 加密密钥索引 │ │
│ │ ├── 加密后的PIN值 │ │
│ │ └── IV/Nonce │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 验证数据 │ │
│ │ │ │
│ │ ├── Salt │ │
│ │ ├── Hash算法 │ │
│ │ └── Hash值(用于快速验证PIN) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
PIN验证流程:
PIN验证流程:
应用程序输入PIN
│
│ C_Login(hSession, CKU_USER, pin, pinLen)
▼
SoftHSM2接收PIN
│
│ 1. 计算PIN的Hash(使用Blob中的Salt)
▼
比较Hash值
│
│ 如果匹配:
│ ├── 解密PIN Blob
│ ├── 获得实际PIN值
│ └── 使用PIN进行后续操作
│
│ 如果不匹配:
│ └── 返回CKR_PIN_INCORRECT
▼
验证完成
Token Flags管理
Token Flags记录Token的状态:
bool OSToken::getTokenFlags(CK_ULONG& flags)
{
OSAttribute attr = tokenObject->getAttribute(CKA_TOKEN_FLAGS);
flags = attr.getUnsignedLongValue();
return true;
}
bool OSToken::setTokenFlags(const CK_ULONG flags)
{
return tokenObject->setAttribute(CKA_TOKEN_FLAGS, OSAttribute(flags));
}
Token Flags定义:
Token Flags定义:
CKF_RNG 有随机数生成器
CKF_WRITE_PROTECTED 写保护
CKF_LOGIN_REQUIRED 需要登录
CKF_USER_PIN_INITIALIZED 用户PIN已初始化
CKF_RESTORE_KEY_NOT_NEEDED 不需要恢复密钥
CKF_CLOCK_ON_TOKEN 有时钟
CKF_PROTECTED_AUTHENTICATION_PATH 保护认证路径
CKF_DUAL_CRYPTO_OPERATIONS 双重密码操作
CKF_TOKEN_INITIALIZED Token已初始化
CKF_SECONDARY_AUTHENTICATION 二级认证
CKF_USER_PIN_COUNT_LOW 用户PIN计数低
CKF_USER_PIN_FINAL_TRY 用户PIN最后一次尝试
CKF_USER_PIN_LOCKED 用户PIN锁定
CKF_SO_PIN_COUNT_LOW SO PIN计数低
CKF_SO_PIN_FINAL_TRY SO PIN最后一次尝试
CKF_SO_PIN_LOCKED SO PIN锁定
CKF_SO_PIN_TO_BE_CHANGED SO PIN需要更改
CKF_USER_PIN_TO_BE_CHANGED 用户PIN需要更改
Token初始化流程
Token初始化创建Token目录和元数据:
OSToken* OSToken::createToken(const std::string basePath,
const std::string tokenDir,
int umask,
const ByteString& label,
const ByteString& serial)
{
std::string tokenPath = basePath + "/" + tokenDir;
// 1. 创建Token目录
Directory dir(tokenPath);
if (!dir.createDirectory()) {
return NULL;
}
// 2. 创建Generation文件
Generation gen(tokenPath + "/generation");
if (!gen.create()) {
return NULL;
}
// 3. 创建Token Object文件
ObjectFile* tokenObject = new ObjectFile(NULL,
tokenPath + "/token.object",
umask,
tokenPath + "/token.lock",
true);
// 4. 设置Token属性
tokenObject->startTransaction();
tokenObject->setAttribute(CKA_LABEL, OSAttribute(label));
tokenObject->setAttribute(CKA_SERIAL_NUMBER, OSAttribute(serial));
tokenObject->setAttribute(CKA_TOKEN_FLAGS, OSAttribute(0));
tokenObject->commitTransaction();
// 5. 创建OSToken实例
OSToken* token = new OSToken(tokenPath, label, umask, serial);
token->tokenObject = tokenObject;
return token;
}
Token对象索引
OSToken需要枚举Token目录中的所有Object:
bool OSToken::index(bool isFirstTime)
{
MutexLocker lock(tokenMutex);
// 1. 获取目录中的文件列表
Directory dir(tokenPath);
std::list<std::string> files = dir.getFiles();
// 2. 更新当前文件集合
std::set<std::string> newFiles;
for (const std::string& file : files) {
if (file.find(".object") != std::string::npos) {
newFiles.insert(file);
}
}
// 3. 检查新增文件
for (const std::string& file : newFiles) {
if (currentFiles.find(file) == currentFiles.end()) {
// 新文件,创建ObjectFile
ObjectFile* obj = new ObjectFile(this,
tokenPath + "/" + file,
umask,
tokenPath + "/" + file + ".lock");
if (obj->isValid()) {
objects.insert(obj);
allObjects.insert(obj);
}
}
}
// 4. 检查删除文件
for (const std::string& file : currentFiles) {
if (newFiles.find(file) == newFiles.end()) {
// 文件被删除,使Object无效
for (auto it = objects.begin(); it != objects.end(); ) {
ObjectFile* obj = (ObjectFile*)*it;
if (obj->getFilename() == file) {
obj->invalidate();
it = objects.erase(it);
} else {
++it;
}
}
}
}
// 5. 更新当前文件集合
currentFiles = newFiles;
return true;
}
DBToken:数据库存储实现
DBToken使用SQLite数据库存储Token:
class DBToken : public ObjectStoreToken
{
public:
DBToken(const std::string &baseDir, const std::string &tokenName,
int umask, const ByteString& label, const ByteString& serial);
DBToken(const std::string &baseDir, const std::string &tokenName, int umask);
static DBToken* createToken(...);
static DBToken* accessToken(...);
virtual bool setSOPIN(const ByteString& soPINBlob);
virtual bool getSOPIN(ByteString& soPINBlob);
virtual bool setUserPIN(ByteString userPINBlob);
virtual bool getUserPIN(ByteString& userPINBlob);
virtual std::set<OSObject*> getObjects();
virtual OSObject* createObject();
virtual bool deleteObject(OSObject* object);
virtual bool isValid();
virtual bool clearToken();
private:
DB::Connection *_connection;
std::map<long long, OSObject*> _allObjects;
Mutex* _tokenMutex;
};
数据库结构:
SQLite数据库表结构:
┌─────────────────────────────────────────────────────────────┐
│ SQLite Database │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ token表 │ │
│ │ ├── label │ │
│ │ ├── serial │ │
│ │ ├── flags │ │
│ │ ├── so_pin │ │
│ │ ├── user_pin │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ objects表 │ │
│ │ ├── id (主键) │ │
│ │ ├── uuid │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ attributes_bool表 │ │
│ │ ├── object_id │ │
│ │ ├── type │ │
│ │ ├── value │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ attributes_ulong表 │ │
│ │ ├── object_id │ │
│ │ ├── type │ │
│ │ ├── value │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ attributes_bytes表 │ │
│ │ ├── object_id │ │
│ │ ├── type │ │
│ │ ├── value │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
DBObject:数据库Object实现
DBObject将属性存储在数据库表中:
class DBObject : public OSObject
{
public:
DBObject(DB::Connection* connection, long long id);
virtual bool attributeExists(CK_ATTRIBUTE_TYPE type);
virtual OSAttribute getAttribute(CK_ATTRIBUTE_TYPE type);
virtual bool setAttribute(CK_ATTRIBUTE_TYPE type, const OSAttribute& attribute);
virtual bool deleteAttribute(CK_ATTRIBUTE_TYPE type);
virtual bool isValid();
virtual bool destroyObject();
private:
DB::Connection* connection;
long long objectId;
bool valid;
Mutex* objectMutex;
};
属性读写:
bool DBObject::setAttribute(CK_ATTRIBUTE_TYPE type, const OSAttribute& attribute)
{
// 根据属性类型选择表
std::string tableName;
if (attribute.isBooleanAttribute()) {
tableName = "attributes_bool";
} else if (attribute.isUnsignedLongAttribute()) {
tableName = "attributes_ulong";
} else {
tableName = "attributes_bytes";
}
// 执行SQL
std::string sql = "INSERT OR REPLACE INTO " + tableName +
" (object_id, type, value) VALUES (?, ?, ?)";
// ...
return true;
}
OSAttribute DBObject::getAttribute(CK_ATTRIBUTE_TYPE type)
{
// 尝试从各个表获取
// 先尝试bool表
// ...
// 再尝试ulong表
// ...
// 最后尝试bytes表
// ...
return OSAttribute();
}
文件存储 vs 数据库存储
两种存储方式的对比:
文件存储 vs 数据库存储对比:
特性 文件存储(OSToken) 数据库存储(DBToken)
─────────────────────────────────────────────────────────────────
实现复杂度 简单 复杂
性能 较低(多文件) 较高(单数据库)
并发性能 较差(文件锁) 较好(事务)
数据完整性 较低 较高
备份恢复 简单(复制目录) 简单(复制数据库)
跨平台 很好 需SQLite支持
适用场景 小规模、简单场景 大规模、高性能场景
─────────────────────────────────────────────────────────────────
Token销毁流程
Token销毁清空Token内容:
bool OSToken::clearToken()
{
MutexLocker lock(tokenMutex);
// 1. 使所有Object无效
for (OSObject* obj : objects) {
obj->destroyObject();
}
objects.clear();
// 2. 删除Token目录中的所有文件
Directory dir(tokenPath);
std::list<std::string> files = dir.getFiles();
for (const std::string& file : files) {
File::remove(tokenPath + "/" + file);
}
// 3. 删除Generation文件
if (gen != NULL) {
gen->reset();
}
// 4. 使Token无效
valid = false;
return true;
}
bool OSToken::resetToken(const ByteString& label)
{
MutexLocker lock(tokenMutex);
// 1. 清空Token
clearToken();
// 2. 重新创建Token Object
tokenObject = new ObjectFile(NULL,
tokenPath + "/token.object",
umask,
tokenPath + "/token.lock",
true);
// 3. 设置新属性
tokenObject->startTransaction();
tokenObject->setAttribute(CKA_LABEL, OSAttribute(label));
tokenObject->setAttribute(CKA_TOKEN_FLAGS, OSAttribute(0));
tokenObject->commitTransaction();
// 4. 使Token有效
valid = true;
return true;
}
一个类比:保险箱库的管理
保险箱库管理类比:
保险箱库(Token)
│
├── 库记录簿(token.object文件)
│ ├── 库名称(Label)
│ ├── 库编号(Serial)
│ ├── 库状态(Flags)
│ │ ├── 是否开放
│ │ ├── 是否锁定
│ │ ├── 是否需要管理员在场
│ │
│ ├── 管理员密码(SO PIN Blob)
│ │ ├── 加密存储
│ │ ├── 带验证Hash
│ │
│ └── 客户密码(User PIN Blob)
│ ├── 加密存储
│ ├── 带验证Hash
│
├── 保险箱存储方式
│ │
│ ├── 文件存储方式(OSToken)
│ │ ├── 每个保险箱一个文件柜
│ │ ├── 内容直接写入文件
│ │ ├── 简单但效率较低
│ │
│ └── 数据库存储方式(DBToken)
│ ├── 所有保险箱记录在一个大账本
│ ├── 按类型分页存储属性
│ ├── 复杂但效率较高
│
└── 库管理操作
├── 创建库(createToken)
├── 清空库(clearToken)
├── 重置库(resetToken)
└── 销毁库(destroyToken)
本篇小结
今天我们分析了Token存储实现。
ObjectStoreToken接口:
- 定义Token存储的统一接口
- 支持PIN、Flags、Label等元数据管理
- 支持Object创建/删除
两种实现:
- OSToken:文件存储,每个Object一个文件
- DBToken:数据库存储,所有属性在表中
PIN存储:
- 不是明文存储
- 以Blob形式加密存储
- 包含Hash用于快速验证
Token Flags:
- 记录Token状态(初始化、锁定等)
- 记录PIN状态(低计数、锁定等)
后端选择:
- 配置文件指定backend类型
- file(默认)或db
下一节,我们将分析密码模块架构——CryptoFactory如何抽象密码后端。
【下集预告】
SoftHSM2如何实现密码算法?
CryptoFactory是什么?
Botan和OpenSSL如何被集成?
AES、RSA、SHA如何实现?
下一节,CryptoFactory架构。