Move学习笔记:Sui Seal的基本使用

  • hwen
  • 发布于 11小时前
  • 阅读 98

使用seal进行加密和解密的基本流程

前言

Seal Gihub Repository: https://github.com/MystenLabs/seal

Seal使用的流程主要包括

  1. 部署合约module,定义seal_approve访问控制函数
  2. 前端配置KeyServer
  3. 加密:生成seal id,调用seal sdk的encrypt函数进行数据加密
  4. 解密:获取用户sessionKey,同时生成一个Transaction对象用于访问seal_approve,再调用seal sdk的decrypt函数进行解密
    1. 收到解密请求的KeySever会访问seal_approve,进行一次dryRun,如果通过,则返回密钥

Move合约部分

在合约中定义一个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);
}

Typescript 前端

首先安装seal库 npm install @mysten/seal

配置SealClient

使用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()获得官方推荐的服务器名单,当然也可以自己定义一组服务器列表,让用户选择

Encryption 加密

// 生成随机数 - 用于创建唯一的文件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函数的package
  • id: seal id,自己生成,用于seal_approve的验证
    • Seal的身份ID是一个vector<u8>类型,可以是[PkgId] + [BCS序列化后的内容],例如如时间戳、用户地址、投票ID等
  • data:需要加密的信息

Decryption 解密

解密需要一个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

执行代码后会弹出类似窗口:

截图_20250619192205.png

接着新建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格式显示

image.png

(前端组件的代码略)点击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信息,对密文进行解密

image.png

用户输入相关信息后,点击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文件夹中找到。

  • 原创
  • 学分: 2
  • 分类: Sui
  • 标签: seal 
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
hwen
hwen
0x04cc...e629
区块链入门中,正在学习Solidity以及Move合约开发,持续发布学习笔记