Notes on the implementation of encryption in Android 3.0

Android加密,挺实用一功能,e文看着费劲,磕磕巴巴翻译一下。

-----------------------------------------------------------------------------

原文地址:

http://source.android.com/tech/encryption/android_crypto_implementation.html

 

 

 

Notes on the implementation of encryption in Android 3.0
Android 3.0 上的磁盘加密


Quick summary for 3rd parties. / 摘要:

If you want to enable encryption on your device based on Android 3.0 aka Honeycomb, there are only a few requirements:

如果你想在3.0 Honeycomb设备上打开加密功能,那么需要:

  1. The /data filesystem must be on a device that presents a block device interface. eMMC is used in the first devices. This is because the encryption is done by the dm-crypt layer in the kernel, which works at the block device layer.

    /data文件系统必须基于块设备,eMMC是首选。这是因为磁盘加密功能工作于kernel的dm-crypt模块,工作在块设备层。

  2. The function get_fs_size() in system/vold/cryptfs.c assumes the filesystem used for /data is ext4. It's just error checking code to make sure the filesystem doesn't extend into the last 16 Kbytes of the partition where the crypto footer is kept. It was useful for development when sizes were changing, but should not be required for release. If you are not using ext4, you can either delete it and the call to it, or fix it to understand the filesystem you are using.

    system/vold/cryptfs.c中的get_fs_size() 函数会假定/data的文件系统是ext4,异常检查确定文件系统没有使用分区上最后16Kbytes,以确保crypto footer的存储空间。对开发人员而言是有用的,因为它的大小会变化,但是无法释放它。如果你用的不是ext4,要么删除这个函数及其调用,要么改造它以适用你的系统。

  3. Most of the code to handle the setup and teardown of the temporary framework is in files that are not usually required to be changed on a per device basis. However, the init..rc file will require some changes. All services must be put in one of three classes: core, main or late_state. Services in the core class are not shutdown and restarted when the temporary framework gets the disk password. Services in the main class are restarted when the framework is restarted. Services in late_start are not started until after the temporary framework is restarted. Put services here that are not required to be running while the temporary framework gets the disk password.

    Also any directories that need to be created on /data that are device specific need to be in the Action for post-fs-data, and that Action must end with the command "setprop vold.post_fs_data_done 1". If your init..rc file does not have a post-fs-data Action, then the post-fs-data Action in the main init.rc file must end with the command "setprop vold.post_fs_data_done 1".

    大部分控制临时framework的设置和代码是在那些通常和设备无关的代码文件中。但是init.<device>.rc需要一些修改。所有的services必须属于三个类中的一个:core,main或late_state. core类中的所有服务在临时 framework 获得磁盘密码时不会停止或重启。main 类中的service会在真正的framwork重启时重启,late_start中的服务在临时framework重启时不会启动,放在late_start中的服务在临时framework获得磁盘密码的过程中不会运行。

    需要在/data目录下创建的所有目录都是需要写在post-fs-data的action,并且必须用命令"setprop vold.post_fs_data_done 1" 来结束。如果你的init.<device>.rc文件没有post-fs-data action,那么主init.rc的post-fs-data action结束时必须执行"setprop vold.post_fs_data_done 1"。

    PS:vold会在停止main class服务后将/data mount为tmpfs,然后触发post-fs-data action的操作并等待"setprop vold.post_fs_data_done 1",然后继续启动临时framework并开始加密过程。如果init.rc中不执行"setprop vold.post_fs_data_done 1",那么vold会认为异常从而重启整个系统。
     

How Android encryption works / Android加密如何工作

Disk encryption on Android is based on dm-crypt, which is a kernel feature that works at the block device layer. Therefore, it is not usable with YAFFS, which talks directly to a raw nand flash chip, but does work with emmc and similar flash devices which present themselves to the kernel as a block device. The current preferred filesystem to use on these devices is ext4, though that is independent of whether encryption is used or not.

Android的磁盘加密基于dm-crypt,它是一个kernel的一个功能动作于块设备上。因此不能基于YAFFS文件系统,它会直接访问NAND Flash芯片,但是可以工作在eMMC及其相似的Flash设备上,只要对于kernel来说是块设备。目前设备上首选使用的文件系统是ext4,但它并不关心加密与否。

While the actual encryption work is a standard linux kernel feature, enabling it on an Android device proved somewhat tricky. The Android system tries to avoid incorporating GPL components, so using the cryptsetup command or libdevmapper were not available options. So making the appropriate ioctl(2) calls into the kernel was the best choice. The Android volume daemon (vold) already did this to support moving apps to the SD card, so I chose to leverage that work for whole disk encryption. The actual encryption used for the filesystem for first release is 128 AES with CBC and ESSIV:SHA256. The master key is encrypted with 128 bit AES via calls to the openssl library.

实际上加密功能是linux kernel的标准功能,但使它能够工作与Android有些棘手。Android尽量避免包含GPL成分,所以不能使用cryptsetup 命令或者libdevmapper。所以使用ioctl(2)通知内核是最好的选择。Android volume daemon(vold)已经支持了apps to SD功能,所以利用它进行整个磁盘的加密。实际用于文件系统加密的首先是128 AES CBC和ESSIV:SHA256。主键通过调用openssl库使用128bit AES加密。

Once it was decided to put the smarts in vold, it became obvious that invoking the encryption features would be done like invoking other vold commands, by adding a new module to vold (called cryptfs) and teaching it various commands. The commands are checkpw, restart, enablecrypto, changepw and cryptocomplete. They will be described in more detail below.

这个功能添加到vold,很明显,调用加密功能将像调用其他vold命令,新的加密模块在vold(称作cryptfs)包含各种命令。命令有checkpw, restart, enablecrypto, changepw 和 cryptocomplete.下面会详细说明。

The other big issue was how to get the password from the user on boot. The initial plan was to implement a minimal UI that could be invoked from init in the initial ramdisk, and then init would decrypt and mount /data. However, the UI engineer said that was a lot of work, and suggested instead that init communicate upon startup to tell the framework to pop up the password entry screen, get the password, and then shutdown and have the real framework started. It was decided to go this route, and this then led to a host of other decisions described below. In particular, init set a property to tell the framework to go into the special password entry mode, and that set the stage for much communication between vold, init and the framework using properties. The details are described below.

另外的一个重要问题是如何在boot阶段得到密码,最初设计在ramdisk实现一个能够被init调用的轻量UI,并且初始化解密功能和mount /data,无论如何,这UI对工程师是大量的工作,取而代之的是init启动临时framework弹出password输入对话框来获得密码,然后停止临时framework并重新启动真的framework,这个方案的确定引出下面的设计。详细点说,init通过设置property告诉framework进入密码输入模式,然后作为和vold沟通的平台,init和framework之间使用properties。详细的之后描述。

Finally, there were problems around killing and restarting various services so that /data could be unmounted and remounted. Bringing up the temporary framework to get the user password requires that a tmpfs /data filesystem be mounted, otherwise the framework will not run. But to unmount the tmpfs /data filesystem so the real decrypted /data filesystem could be mounted meant that every process that had open files on the tmpfs /data filesystem had to be killed and restarted on the real /data filesystem. This magic was accomplished by requiring all services to be in 1 of 3 groups: core, main and late_start. Core services are never shut down after starting. main services are shutdown and then restarted after the disk password is entered. late_start services are not started until after /data has been decrypted and mounted. The magic to trigger these actions is by setting the property vold.decrypt to various magic strings, which is described below. Also, a new init command "class_reset" was invented to stop a service, but allow it to be restarted with a "class_start" command. If the command "class_stop" was used instead of the new command "class_reset" the flag SVC_DISABLED was added to the state of any service stopped, which means it would not be started when the command class_start was used on its class.

最后,会涉及的问题有杀掉及重启不同的服务,/data会卸载再重新加载。启动一个临时framework来获得密码,需要mount /data为tmpfs,否则framework无法启动。但是当umount tmpfs /data然后mount实际的/data加密文件系统时,所有在tmpfs上打开文件的进程会被kill并重启到实际的/data文件系统上。这个逻辑需要所有服务属于1到3组:core, main, late_start. core服务在启动后不会停止,main服务会在输入完磁盘密码后停止并重启,late_start服务直到/data挂载并解密后才会启动。这个逻辑去触发动作是通过设置property vold.decrypt实现,描述在后边。同时,出现一个新的init命令"class_reset" 负责停止一个服务,但是允许它通过"class_start"命令重启。如果用"class_stop"替代"class_reset",会在服务停止状态设置SVC_DISABLED标志,这意味着当服务所属class使用class_start时,它不会被启动。

Booting an encrypted system.  引导加密系统

  1. When init fails to mount /data, it assumes the filesystem is encrypted, and sets several properties: ro.crypto.state = "encrypted" vold.decrypt = 1 It then mounts a /data on a tmpfs ramdisk, using parameters it picks up from ro.crypto.tmpfs_options, which is set in init.rc.

    If init was able to mount /data, it sets ro.crypto.state to "unencrypted".

    In either case, init then sets 5 properties to save the initial mount options given for /data in these properties: ro.crypto.fs_type ro.crypto.fs_real_blkdev ro.crypto.fs_mnt_point ro.crypto.fs_options ro.crypto.fs_flags (saved as an ascii 8 digit hex number preceded by 0x)

    当init mount /data失败,它假定文件系统已经加密,并设置几个properties:ro.crypto.state = "encrypted" vold.decrypt = 1 表明/data被mount为tmpfs ramdisk,使用init.rc设置的参数ro.crypto.tmpfs_options。

    如果init能够mount /data,会设置ro.crypto.state = "unencrypted".

    无论如何,对于/data,init会设置5个properties来保存初始mount选项:ro.crypto.fs_type ro.crypto.fs_real_blkdev ro.crypto.fs_mnt_point ro.crypto.fs_options ro.crypto.fs_flags (0x开头的8个ascii字符的16进制数字)

  2. The framework starts up, and sees that vold.decrypt is set to "1". This tells the framework that it is booting on a tmpfs /data disk, and it needs to get the user password. First, however, it needs to make sure that the disk was properly encrypted. It sends the command "cryptfs cryptocomplete" to vold, and vold returns 0 if encryption was completed successfully, or -1 on internal error, or -2 if encryption was not completed successfully. Vold determines this by looking in the crypto footer for the CRYPTO_ENCRYPTION_IN_PROGRESS flag. If it's set, the encryption process was interrupted, and there is no usable data on the device. If vold returns an error, the UI should pop up a message saying the user needs to reboot and factory reset the device, and give the user a button to press to do so.

    framework启动时会判断 vold.decrypt,如果设置成 "1",那么 framework 知道 /data 被mount为tmpfs,必须询问用户密码。 首先,需要先要确定disk是否处于加密状态,通过给 vold 发送命令 "cryptfs cryptocomplete",vold 返回 0 表示加密状态,返回 -1 表示内部错误,-2 表示加密没有完成。Vold 通过crypto footer的CRYPTO_ENCRYPTION_IN_PROGRESS flag来确定。如果它被设置了,那么表示加密过程被中断了,没有可用的存储设备。当 vold 返回错误时,UI应该弹出 message 通知用户必须 reboot 并 factory reset 设备,没有别的选择。

  3. Assuming the "cryptfs cryptocomplete" command returned success, the framework should pop up a UI asking for the disk password. The UI then sends the command "cryptfs checkpw " to vold. If the password is correct (which is determined by successfully mounting the decrypted at a temporary location, then unmounting it), vold saves the name of the decrypted block device in the property ro.crypto.fs_crypto_blkdev, and returns status 0 to the UI. If the password is incorrect, it returns -1 to the UI.

    假设"cryptfs cryptocomplete" 命令返回成果,那么framework 应该弹出UI询问用户磁盘密码,这个UI会发送命令"cryptfs checkpw " 给vold,如果密码正确 (够成功mount磁盘到临时路径,然后再unmount), vold 保存加密块设备的名字到ro.crypto.fs_crypto_blkdev然后返回 0 给 UI,如果密码错误返回 -1 给UI。

  4. The UI puts up a crypto boot graphic, and then calls vold with the command "cryptfs restart". vold sets the property vold.decrypt to "trigger_reset_main", which causes init.rc to do "class_reset main". This stops all services in the main class, which allows the tmpfs /data to be unmounted. vold then mounts the decrypted real /data partition, and then preps the new partition (which may never have been prepped if it was encrypted with the wipe option, which is not supported on first release). It sets the property vold.post_fs_data_done to "0", and then sets vold.decrypt to "trigger_post_fs_dat". This causes init.rc to run the post-fs-data commands in init.rc and init..rc. They will create any necessary directories, links, et al, and then set vold.post_fs_data_done to "1". Vold waits until it sees the "1" in that property. Finally, vold sets the property vold.decrypt to "trigger_restart_framework" which causes init.rc to start services in class main again, and also start services in class late_start for the first time since boot.

    Now the framework boots all its services using the decrypted /data filesystem, and the system is ready for use.

    UI显示一个加密启动画面,然后调用"cryptfs restart",vold设置property vold.decrypt = "trigger_reset_main",关联 init.rc 的 "class_reset main",会导致停止所有属于main class的 services,允许unmount tmpfs的/data,vold 会mount被加密的真实/data分区,并做相应准备工作 (如果通过wipe选项加密,data会被清除,需要重新加载数据。不支持首次启动)。然后设置vold.post_fs_data_done = "0" 和 vold.decrypt = "trigger_post_fs_dat", 它导致init.rc和init.<device>.rc 执行 post-fs-data 命令,创建相关的目录、连接及设置vold.post_fs_data_done = "1",Vold 会等待这个标志。最后 vold 设置 vold.decrypt = "trigger_restart_framework" ,通知 init.rc 启动main class和late_start class的services等首次启动的服务。

    现在,framework 已经使用加密的/data文件系统启动完成并准备好。

Enabling encryption on the device. / 在设备上启用加密

For first release, we only support encrypt in place, which requires the framework to be shutdown, /data unmounted, and then every sector of the device encrypted, after which the device reboots to go through the process described above. Here are the details:

首次发布时,我们只支持inplace加密,要求framework shutdown,/data unmount,然后加密每个区块,以上操作需要reboot设备之后开始。详细如下:

  1. From the UI, the user selects to encrypt the device. The UI ensures that there is a full charge on the battery, and the AC adapter is plugged in. It does this to make sure there is enough power to finish the encryption process, because if the device runs out of power and shuts down before it has finished encrypting, file data is left in a partially encrypted state, and the device must be factory reset (and all data lost).

    Once the user presses the final button to encrypt the device, the UI calls vold with the command "cryptfs enablecrypto inplace " where passwd is the user's lock screen password.

    用户从UI选择加密设备,UI确认电池电量充足并且插入电源, 以确保有足够电量完成加密过程,因为如果设备在加密过程完成前因为没电而关机,数据会停留在一个不完整的加密状态,设备必须进行factory reset(所有数据将丢失)。

    一旦用户按下按钮开始加密设备,UI将通过给vold发送命令 "cryptfs enablecrypto inplace ",密码使用用户锁屏密码。
  2. vold does some error checking, and returns -1 if it can't encrypt, and prints a reason in the log. If it thinks it can, it sets the property vold.decrypt to "trigger_shutdown_framework". This causes init.rc to stop services in the classes late_start and main. vold then unmounts /mnt/sdcard and then /data.

    vold会进行一些错误检查,返回 -1 代表不能加密并输出包含原因的log,如果确定可以加密,它会设置vold.decrypt = "trigger_shutdown_framework",将触发init.rc停止属于late_start和main的服务进程。vold会unmount /mnt/sdcard 和 /data
  3. If doing an inplace encryption, vold then mounts a tmpfs /data (using the tmpfs options from ro.crypto.tmpfs_options) and sets the property vold.encrypt_progress to "0". It then preps the tmpfs /data filesystem as mentioned in step 3 for booting an encrypted system, and then sets the property vold.decrypt to "trigger_restart_min_framework". This causes init.rc to start the main class of services. When the framework sees that vold.encrypt_progress is set to "0", it will bring up the progress bar UI, which queries that property every 5 seconds and updates a progress bar.

    如果进行一个inplace加密,vold会mount /data成为tmpfs(使用tmpfs选项从 ro.crypto.tmpfs_options )并且设置vold.encrypt_progress = "0",为了启动一个能够进行加密系统,它会准备tmpfs的/data在这步,并且设置 vold.decrypt = "trigger_restart_min_framework",这将触发init.rc启动main class的服务进程,当framework看到 vold.encrypt_progress 设置为 "0",会启动显示进度的UI,每5s刷新一次进度。
  4. vold then sets up the crypto mapping, which creates a virtual crypto block device that maps onto the real block device, but encrypts each sector as it is written, and decrypts each sector as it is read. vold then creates and writes out the crypto footer.

    The crypto footer contains details on the type of encryption, and an encrypted copy of the master key to decrypt the filesystem. The master key is a 128 bit number created by reading from /dev/urandom. It is encrypted with a hash of the user password created with the PBKDF2 function from the SSL library. The footer also contains a random salt (also read from /dev/urandom) used to add entropy to the hash from PBKDF2, and prevent rainbow table attacks on the password. Also, the flag CRYPT_ENCRYPTION_IN_PROGRESS is set in the crypto footer to detect failure to complete the encryption process. See the file cryptfs.h for details on the crypto footer layout. The crypto footer is kept in the last 16 Kbytes of the partition, and the /data filesystem cannot extend into that part of the partition.

    之后vold设置加密mapping,它创建了一个虚拟的加密块设备映射到实际的块设备,但加密是一块块写入,解密是一块块读取,vold会创建并写入 crypto footer 

    crypto footer包含了加密类型的详细信息和加密秘钥,用于系统解密。主密钥通过读取 /dev/urandom 创建的128bit随机值,对用户密码进行哈希加密,加密算法使用SSL库的PBKDF2函数。 footer 也同样包含随机数(也从 /dev/urandom 读取), 为了增加熵而使用PBKDF2的hash算法得出,防止暴力穷举破解密码。同时,CRYPT_ENCRYPTION_IN_PROGRESS标志设置到crypto footer中防止加密失败,cryptfs.h中有详细的crypto footer布局,crypto footer保存在分区的最后16Kbytes上,且/data文件系统不能使用到这部分。
  5. If told was to enable encryption with wipe, vold invokes the command "make_ext4fs" on the crypto block device, taking care to not include the last 16 Kbytes of the partition in the filesystem.

    If the command was to enable inplace, vold starts a loop to read each sector of the real block device, and then write it to the crypto block device. This takes about an hour on a 30 Gbyte partition on the Motorola Xoom. This will vary on other hardware. The loop updates the property vold.encrypt_progress every time it encrypts another 1 percent of the partition. The UI checks this property every 5 seconds and updates the progress bar when it changes.

    如果是wipe模式的加密,vold会调用命令 "make_ext4fs" 在这个加密块设备,分区上不会包含那最后的16Kbytes。

    如果是inplace模式,vold开始循环读取每块数据然后写入加密块设备,这在30Gbyte分区的Motorola Xoom上要花费约1小时。这取决于硬件。 加密进度每增加1% 会更新
    vold.encrypt_progress 。UI会以5s间隔检查加密进度的变化。
  6. When either encryption method has finished successfully, vold clears the flag ENCRYPTION_IN_PROGRESS in the footer, and reboots the system. If the reboot fails for some reason, vold sets the property vold.encrypt_progress to "error_reboot_failed" and the UI should display a message asking the user to press a button to reboot. This is not expected to ever occur.

    当各种加密全部成功时,vold会清除 footer 上的 ENCRYPTION_IN_PROGRESS 标志,然后reboot系统,如果因为某些原因reboot失败,vold会设置 vold.encrypt_progress = "error_reboot_failed" ,UI告知用户强行reboot,这不是预期会发生的情况。
  7. If vold detects an error during the encryption process, and if no data has been destroyed yet and the framework is up, vold sets the property vold.encrypt_progress to "error_not_encrypted" and the UI should give the user the option to reboot, telling them that the encryption process never started. If the error occurs after the framework has been torn down, but before the progress bar UI is up, vold will just reboot the system. If the reboot fails, it sets vold.encrypt_progress to "error_shutting_down" and returns -1, but there will not be anyone to catch the error. This is not expected to happen.

    If vold detects an error during the encryption process, it sets vold.encrypt_progress to "error_partially_encrypted" and returns -1. The UI should then display a message saying the encryption failed, and provide a button for the user to factory reset the device.

    如果 vold 在加密过程中遇到错误,没有数据遭到破坏并且framework在运行,vold会设置 vold.encrypt_progress = "error_not_encrypted" ,UI提示用户reboot并告知加密没有进行。如果错误发生在framework停止之后,但是在UI进度条运行之前,vold会reboot系统,如果reboot失败,会设置 vold.encrypt_progress = "error_shutting_down" 并返回 -1,但不会有谁捕捉这个错误,这不是预期发生的。

    如果vold在加密过程中遇到错误,设置vold.encrypt_progress = “ error_partially_encrypted ”,并返回-1 。然后, UI显示消息说明加密失败,并为用户提供恢复出厂设置的按钮。

Changing the password / 改变密码

To change the password for the disk encryption, the UI sends the command "cryptfs changepw " to vold, and vold re-encrypts the disk master key with the new password.

改变磁盘加密的密码时,UI发送命令"cryptfs changepw" 给 vold,vold用disk master key重新加密。

Summary of related properties / 相关properties描述

Here is a table summarizing the various properties, their possible values, and what they mean:

以下表格是关联properties、键值及描述:

vold.decrypt                               Set by init to tell the UI to ask
                                              for the disk pw

vold.decrypt  trigger_reset_main              Set by vold to shutdown the UI
                                              asking for the disk password

vold.decrypt  trigger_post_fs_data            Set by vold to prep /data with
                                              necessary dirs, et al.

vold.decrypt  trigger_restart_framework       Set by vold to start the real
                                              framework and all services

vold.decrypt  trigger_shutdown_framework      Set by vold to shutdown the full
                                              framework to start encryption

vold.decrypt  trigger_restart_min_framework   Set by vold to start the progress
                                              bar UI for encryption.

vold.enrypt_progress                          When the framework starts up, if
                                              this property is set, enter the
                                              progress bar UI mode.

vold.encrypt_progress  0 to 100               The progress bar UI should display
                                              the percentage value set.

vold.encrypt_progress  error_partially_encrypted  The progress bar UI should
                                                  display a message that the
                                                  encryption failed, and give
                                                  the user an option to factory
                                                  reset the device.

vold.encrypt_progress  error_reboot_failed    The progress bar UI should display
                                              a message saying encryption
                                              completed, and give the user a
                                              button to reboot the device.
                                              This error is not expected to
                                              happen.

vold.encrypt_progress  error_not_encrypted    The progress bar UI should display
                                              a message saying an error occured,
                                              and no data was encrypted or lost,
                                              and give the user a button to
                                              reboot the system.

vold.encrypt_progress  error_shutting_down    The progress bar UI is not
                                              running, so it's unclear who
                                              will respond to this error,
                                              and it should never happen
                                              anyway.

vold.post_fs_data_done                     Set by vold just before setting
                                              vold.decrypt to
                                              trigger_post_fs_data.

vold.post_fs_data_done                     Set by init.rc or init.<device>.rc
                                              just after finishing the task
                                              post-fs-data.

ro.crypto.fs_crypto_blkdev                    Set by the vold command checkpw
                                              for later use by the vold command
                                              restart.

ro.crypto.state unencrypted                   Set by init to say this system is
                                              running with an unencrypted /data

ro.crypto.state encrypted                     Set by init to say this system is
                                              running with an encrypted /data

ro.crypto.fs_type                             These 5 properties are set by init
ro.crypto.fs_real_blkdev                      when it tries to mount /data with
ro.crypto.fs_mnt_point                        parameters passed in from init.rc.
ro.crypto.fs_options                          vold uses these to setup the
ro.crypto.fs_flags                            crypto mapping.

ro.crypto.tmpfs_options                       Set by init.rc with the options
                                              init should use when mounting
                                              the tmpfs /data filesystem

Summary of new init actions

A list of the new Actions that are added to init.rc and/or init..rc:

增加到init.rc init.<device>.rc的新的action

on post-fs-data
on nonencrypted
on property:vold.decrypt=trigger_reset_main
on property:vold.decrypt=trigger_post_fs_data
on property:vold.decrypt=trigger_restart_min_framework
on property:vold.decrypt=trigger_restart_framework
on property:vold.decrypt=trigger_shutdown_framework

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值