使用seal进行加密和解密的基本流程
Seal Gihub Repository: https://github.com/MystenLabs/seal
Seal使用的流程主要包括
seal_approve
访问控制函数KeyServer
encrypt
函数进行数据加密sessionKey
,同时生成一个Transaction
对象用于访问seal_approve
,再调用seal sdk的decrypt
函数进行解密
seal_approve
,进行一次dryRun,如果通过,则返回密钥在合约中定义一个seal_approve
函数,来进行访问控制,每次需要进行解密时,seal都会先访问该合约函数,如果通过,才会给用户返回解密密钥
编写seal_approve
函数需要注意:
package
内可以有多个seal_approve函数,存在于不同的module
中seal_approve
最好是non-public entry
seal_approve
的第一个参数必须是seal id,id: vector<u8>
seal_approve
应触发assert
而不返回任何值seal_approve
函数必须是无副作用的,即不能对链上状态进行修改下边的例子中,seal_approve
检测用户是否在allowlist中,如果通过,则不会触发任何assert
错误
/// All allowlisted addresses can access all IDs with the prefix of the allowlist
fun approve_internal(caller: address, id: vector<u8>, allowlist: &Allowlist): bool {
// Check if the id has the right prefix
let namespace = namespace(allowlist);
if (!is_prefix(namespace, id)) {
return false
};
// Check if user is in the allowlist
allowlist.list.contains(&caller)
}
entry fun seal_approve(id: vector<u8>, allowlist: &Allowlist, ctx: &TxContext) {
assert!(approve_internal(ctx.sender(), id, allowlist), ENoAccess);
}
首先安装seal库 npm install @mysten/seal
使用seal前需要配置seal密钥服务器KeyServer
,负责数据密钥的分片和重构
//配置sui客户端
const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') });
const sealClient = new SealClient({
suiClient,
serverConfigs: getAllowlistedKeyServers('testnet').map((id) => ({
objectId: id,
weight: 1,
})),
verifyKeyServers: false,
});
export {suiClient,sealClient}
weight
表示一个key server权重更高,可以在选择列表中重复出现verifyKeyServers
验证是否为密钥服务器,适合在app启动时运行一次用于验证getAllowlistedKeyServers()
获得官方推荐的服务器名单,当然也可以自己定义一组服务器列表,让用户选择// 生成随机数 - 用于创建唯一的文件ID
const nonce = crypto.getRandomValues(new Uint8Array(5));
const policyObjectBytes = fromHex(policyObject);
const id = toHex(new Uint8Array([...policyObjectBytes, ...nonce]));
const { encryptedObject: encryptedBytes, key: backupKey } = await sealClient.encrypt({
threshold: 2,
packageId: fromHEX(packageId),
id: fromHEX(id),
data,
});
packageId
: 包含了seal_approve
函数的packageid
: seal id,自己生成,用于seal_approve
的验证
vector<u8>
类型,可以是[PkgId] + [BCS序列化后的内容]
,例如如时间戳、用户地址、投票ID等data
:需要加密的信息解密需要一个SessionKey
以获取解密密钥,用户需要用钱包进行签名,签名结果就放在SessionKey中
const sessionKey = await SessionKey.create({
address: suiAddress,
packageId: fromHEX(packageId),
ttlMin: 10, // TTL of 10 minutes
suiClient: new SuiClient({ url: getFullnodeUrl('testnet') }),
});
const message = sessionKey.getPersonalMessage();
const { signature } = await keypair.signPersonalMessage(message); // User confirms in wallet
sessionKey.setPersonalMessageSignature(signature); // Initialization complete
执行代码后会弹出类似窗口:
接着新建Transaction对象,用于与链上seal_approve
函数交互,最后就可以调用decrypt
函数来开始Seal的解密流程了:
// Create the Transaction for evaluating the seal_approve function.
const tx = new Transaction();
tx.moveCall({
target: `${packageId}::${moduleName}::seal_approve`,
arguments: [
tx.pure.vector("u8", fromHEX(id)),
// other arguments
]
});
const txBytes = tx.build( { client: suiClient, onlyTransactionKind: true })
try{
const decryptedBytes = await client.decrypt({
data: encryptedBytes,
sessionKey,
txBytes,
});
}catch(err){
//handle error message
}
当需要多个密钥,且解密逻辑一致时,建议使用fetchKeys函数后,再进行解密,以可以减少对服务器的访问次数
await client.fetchKeys({
ids: [id1, id2],
txBytes: txBytesWithTwoSealApproveCalls,
sessionKey,
threshold: 2,
});
下面展示我自己写的使用seal加密,并通过nft权限验证的一个简单的demo代码,下面是合约中的seal_apporve
函数,用于检测用户传入的NFT类型是否正确:
entry fun seal_approve<T : key>(id: vector<u8>,nft_token: &T,messgaeBox : &MessgaeBox){
assert!(approve_internal(id,nft_token,messgaeBox));
}
fun approve_internal<T : key>(id: vector<u8>,_nft_token: &T,messgaeBox : &MessgaeBox): bool {
let nft_typename = type_name::get<T>();
let namespace = bcs::to_bytes(&nft_typename.get_address());
if (!is_prefix(namespace, id)) {
return false
};
if(nft_typename == messgaeBox.nft_token){
return true
};
false
}
下面是前端代码
index.ts
配置KeyServer
const suiClient = new SuiClient({ url: networkConfig[network].url });
const sealClient = new SealClient({
suiClient,
serverConfigs: getAllowlistedKeyServers('testnet').map(server => ({
objectId: server,
weight: 1
})),
verifyKeyServers: false,
})
页面A:加密信息,指定持有某种类型NFT的用户可以解密。样式如下, 返回的密文由Unit8Array
转化为base64
格式显示
(前端组件的代码略)点击Submit按钮后调用seal加密函数encryptMessage
加密信息,并返回密文。
export const encryptMessage = async (message: string, nft_package_id: string) => {
const nonce = crypto.getRandomValues(new Uint8Array(5));
const policyObjectBytes = new TextEncoder().encode(nft_package_id);
const length_prefix = new Uint8Array([policyObjectBytes.length]);
const id = toHex(new Uint8Array([...length_prefix, ...policyObjectBytes, ...nonce]));
console.log(id);
const encoder = new TextEncoder();
const serializedData = encoder.encode(message);
const { encryptedObject: encryptedBytes } = await sealClient.encrypt({
threshold: 2,
id,
packageId: packageID,
data: serializedData,
});
console.log("Encrypted Success");
const base64 = Buffer.from(encryptedBytes).toString('base64');
return base54;
}
之后密文和NFT信息一起保存上链
页面B,接受密文和NFT信息,对密文进行解密
用户输入相关信息后,点击Submit按钮,先进行签名的检测,再调用解密函数。sessionKey
中的packageID
必须与encrypt
中的一致
const { mutate: signPersonalMessage } = useSignPersonalMessage();
...
const signSessionKeyAndDecrpt = useCallback(async (nft_id: string, nft_type: string, message: string) => {
if (!currentAccount) {
return;
}
try {
//sessionKey是否存在,是否过期
if (currentSessionKey && !currentSessionKey.isExpired) {
console.log(message);
const moveCallConstructor = constructMoveCall(nft_id, vote_pool_id, nft_type);
decryptMessgae(currentSessionKey, new Uint8Array(Buffer.from(message, 'base64')), moveCallConstructor).then((res) => {
if (res) {
console.log(res);
setDecryptedResult(res);
} else console.log("no data");
});
}
else {
const sessionKey = await SessionKey.create({
address: currentAccount.address,
packageId: packageID,
ttlMin: 10,
suiClient: suiClient
});
signPersonalMessage(
{
message: sessionKey.getPersonalMessage(),
},
{
//成功后进行decrypt
onSuccess: async (result: { signature: string }) => {
await sessionKey.setPersonalMessageSignature(result.signature);
setCurrentSessionKey(sessionKey);
console.log(message);
const moveCallConstructor = constructMoveCall(nft_id, vote_pool_id, nft_type);
decryptMessgae(sessionKey, new Uint8Array(Buffer.from(message, 'base64')), moveCallConstructor).then((res) => {
if (res) {
console.log(res);
setDecryptedResult(res);
} else console.log("no data");
});
},
onError: (error) => {
console.error('sign message error:', error);
}
}
);
}
} catch (error) {
console.error('sign message error:', error);
}
}, [currentAccount, signPersonalMessage]);
construcMoveCall
帮助构建解密需要的Transaction对象
const constructMoveCall = (nft_id: string, message_box_id: string, nft_type: string): MoveCallConstructor => {
return (tx: Transaction, id: string) => {
tx.moveCall({
target: `${packageId}::nft_test::seal_approve`,
typeArguments: [nft_type],
arguments: [
tx.pure.vector('u8', fromHex(id)),
tx.object(nft_id),
tx.object(message_box_id)
]
});
};
};
调用的decryptMessage
函数代码:
export type MoveCallConstructor = (tx: Transaction, id: string) => void;
export const decryptMessgae = async (sessionKey: SessionKey, encryptedMessage: Uint8Array, moveCallConstructor: MoveCallConstructor) => {
const fullId = EncryptedObject.parse(new Uint8Array(encryptedMessage)).id;
const tx = new Transaction();
moveCallConstructor(tx, fullId);
const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
try {
const decryptedMessage = await sealClient.decrypt({
data: new Uint8Array(encryptedMessage),
sessionKey,
txBytes,
});
console.log("权限通过");
const decoder = new TextDecoder();
const message = decoder.decode(decryptedMessage); //从Unit8Array转化为String
return message;
} catch (err) {
console.log(err);
const errorMsg =
err instanceof NoAccessError
? 'No access to decryption keys'
: 'Unable to decrypt files, try again';
console.error(errorMsg, err);
return;
}
}
其他简单的项目也可以参考官方的 landing page: Seal landing page ,包含了一个使用allowlist和subscription模式的demo,项目代码可以在官方github repo下的example文件夹中找到。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!