am 8ddbe40a
: Updates to cryptfs framework.
* commit '8ddbe40a8a8708dac7c472fa8c098c8f7b24534c': Updates to cryptfs framework.
This commit is contained in:
commit
934dfed05d
3 changed files with 238 additions and 97 deletions
|
@ -514,8 +514,6 @@ CommandListener::CryptfsCmd::CryptfsCmd() :
|
|||
|
||||
int CommandListener::CryptfsCmd::runCommand(SocketClient *cli,
|
||||
int argc, char **argv) {
|
||||
dumpArgs(argc, argv, -1);
|
||||
|
||||
if (argc < 2) {
|
||||
cli->sendMsg(ResponseCode::CommandSyntaxError, "Missing Argument", false);
|
||||
return 0;
|
||||
|
@ -528,20 +526,31 @@ int CommandListener::CryptfsCmd::runCommand(SocketClient *cli,
|
|||
cli->sendMsg(ResponseCode::CommandSyntaxError, "Usage: cryptfs checkpw <passwd>", false);
|
||||
return 0;
|
||||
}
|
||||
dumpArgs(argc, argv, 2);
|
||||
rc = cryptfs_check_passwd(argv[2]);
|
||||
} else if (!strcmp(argv[1], "restart")) {
|
||||
if (argc != 2) {
|
||||
cli->sendMsg(ResponseCode::CommandSyntaxError, "Usage: cryptfs restart", false);
|
||||
return 0;
|
||||
}
|
||||
dumpArgs(argc, argv, -1);
|
||||
rc = cryptfs_restart();
|
||||
} else if (!strcmp(argv[1], "enablecrypto")) {
|
||||
if ( (argc != 4) || (strcmp(argv[2], "wipe") && strcmp(argv[2], "inplace")) ) {
|
||||
cli->sendMsg(ResponseCode::CommandSyntaxError, "Usage: cryptfs enablecrypto <wipe|inplace> <passwd>", false);
|
||||
return 0;
|
||||
}
|
||||
dumpArgs(argc, argv, 3);
|
||||
rc = cryptfs_enable(argv[2], argv[3]);
|
||||
} else if (!strcmp(argv[1], "changepw")) {
|
||||
if (argc != 4) {
|
||||
cli->sendMsg(ResponseCode::CommandSyntaxError, "Usage: cryptfs changepw <oldpasswd> <newpasswd>", false);
|
||||
return 0;
|
||||
}
|
||||
SLOGD("cryptfs changepw <oldpw> <newpw>");
|
||||
rc = cryptfs_changepw(argv[2], argv[3]);
|
||||
} else {
|
||||
dumpArgs(argc, argv, -1);
|
||||
cli->sendMsg(ResponseCode::CommandSyntaxError, "Unknown cryptfs cmd", false);
|
||||
}
|
||||
|
||||
|
|
263
cryptfs.c
263
cryptfs.c
|
@ -33,6 +33,7 @@
|
|||
#include <string.h>
|
||||
#include <sys/mount.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <errno.h>
|
||||
#include <sys/reboot.h>
|
||||
#include "cryptfs.h"
|
||||
|
@ -41,9 +42,13 @@
|
|||
#include "cutils/properties.h"
|
||||
|
||||
#define DM_CRYPT_BUF_SIZE 4096
|
||||
#define DATA_MNT_POINT "/data"
|
||||
|
||||
char *me = "cryptfs";
|
||||
|
||||
static unsigned char saved_key_sha1[20] = { '\0' };
|
||||
static int key_sha1_saved = 0;
|
||||
|
||||
static void ioctl_init(struct dm_ioctl *io, size_t dataSize, const char *name, unsigned flags)
|
||||
{
|
||||
memset(io, 0, dataSize);
|
||||
|
@ -150,13 +155,7 @@ static int get_crypt_ftr_and_key(char *real_blk_name, struct crypt_mnt_ftr *cryp
|
|||
* encryption info footer and key, and plenty of bytes to spare for future
|
||||
* growth.
|
||||
*/
|
||||
#if 1 /* The real location, use when the enable code works */
|
||||
off = ((off64_t)nr_sec * 512) - CRYPT_FOOTER_OFFSET;
|
||||
#else
|
||||
/* For testing, I'm slapping a handbuild header after my 200 megabyte
|
||||
* /data partition. So my offset if 200 megabytes */
|
||||
off = 200*1024*1024;
|
||||
#endif
|
||||
|
||||
if (lseek64(fd, off, SEEK_SET) == -1) {
|
||||
SLOGE("Cannot seek to real block device footer\n");
|
||||
|
@ -343,36 +342,31 @@ errout:
|
|||
|
||||
}
|
||||
|
||||
/* If we need to debug this, look at Devmapper.cpp:dumpState(),
|
||||
* It does DM_LIST_DEVICES, then iterates on each device and
|
||||
* calls DM_DEV_STATUS.
|
||||
*/
|
||||
|
||||
#define HASH_COUNT 2000
|
||||
#define KEY_LEN_BYTES 16
|
||||
#define IV_LEN_BYTES 16
|
||||
|
||||
static int create_encrypted_random_key(char *passwd, unsigned char *master_key)
|
||||
static void pbkdf2(char *passwd, unsigned char *ikey)
|
||||
{
|
||||
int fd;
|
||||
unsigned char buf[KEY_LEN_BYTES];
|
||||
unsigned char ikey[32+32] = { 0 }; /* Big enough to hold a 256 bit key and 256 bit IV */
|
||||
unsigned char salt[32] = { 0 };
|
||||
EVP_CIPHER_CTX e_ctx;
|
||||
int encrypted_len, final_len;
|
||||
|
||||
/* Get some random bits for a key */
|
||||
fd = open("/dev/urandom", O_RDONLY);
|
||||
read(fd, buf, sizeof(buf));
|
||||
close(fd);
|
||||
|
||||
/* Now encrypt it with the password */
|
||||
/* To Do: Make a salt based on some immutable data about this device.
|
||||
* IMEI, or MEID, or CPU serial number, or whatever we can find
|
||||
*/
|
||||
/* Turn the password into a key and IV that can decrypt the master key */
|
||||
PKCS5_PBKDF2_HMAC_SHA1(passwd, strlen(passwd), salt, sizeof(salt),
|
||||
HASH_COUNT, KEY_LEN_BYTES+IV_LEN_BYTES, ikey);
|
||||
}
|
||||
|
||||
static int encrypt_master_key(char *passwd, unsigned char *decrypted_master_key,
|
||||
unsigned char *encrypted_master_key)
|
||||
{
|
||||
unsigned char ikey[32+32] = { 0 }; /* Big enough to hold a 256 bit key and 256 bit IV */
|
||||
EVP_CIPHER_CTX e_ctx;
|
||||
int encrypted_len, final_len;
|
||||
|
||||
/* Turn the password into a key and IV that can decrypt the master key */
|
||||
pbkdf2(passwd, ikey);
|
||||
|
||||
/* Initialize the decryption engine */
|
||||
if (! EVP_EncryptInit(&e_ctx, EVP_aes_128_cbc(), ikey, ikey+KEY_LEN_BYTES)) {
|
||||
|
@ -380,13 +374,14 @@ static int create_encrypted_random_key(char *passwd, unsigned char *master_key)
|
|||
return -1;
|
||||
}
|
||||
EVP_CIPHER_CTX_set_padding(&e_ctx, 0); /* Turn off padding as our data is block aligned */
|
||||
|
||||
/* Encrypt the master key */
|
||||
if (! EVP_EncryptUpdate(&e_ctx, master_key, &encrypted_len,
|
||||
buf, KEY_LEN_BYTES)) {
|
||||
if (! EVP_EncryptUpdate(&e_ctx, encrypted_master_key, &encrypted_len,
|
||||
decrypted_master_key, KEY_LEN_BYTES)) {
|
||||
SLOGE("EVP_EncryptUpdate failed\n");
|
||||
return -1;
|
||||
}
|
||||
if (! EVP_EncryptFinal(&e_ctx, master_key + encrypted_len, &final_len)) {
|
||||
if (! EVP_EncryptFinal(&e_ctx, encrypted_master_key + encrypted_len, &final_len)) {
|
||||
SLOGE("EVP_EncryptFinal failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
@ -403,16 +398,11 @@ static int decrypt_master_key(char *passwd, unsigned char *encrypted_master_key,
|
|||
unsigned char *decrypted_master_key)
|
||||
{
|
||||
unsigned char ikey[32+32] = { 0 }; /* Big enough to hold a 256 bit key and 256 bit IV */
|
||||
unsigned char salt[32] = { 0 };
|
||||
EVP_CIPHER_CTX d_ctx;
|
||||
int decrypted_len, final_len;
|
||||
|
||||
/* To Do: Make a salt based on some immutable data about this device.
|
||||
* IMEI, or MEID, or CPU serial number, or whatever we can find
|
||||
*/
|
||||
/* Turn the password into a key and IV that can decrypt the master key */
|
||||
PKCS5_PBKDF2_HMAC_SHA1(passwd, strlen(passwd), salt, sizeof(salt),
|
||||
HASH_COUNT, KEY_LEN_BYTES+IV_LEN_BYTES, ikey);
|
||||
pbkdf2(passwd, ikey);
|
||||
|
||||
/* Initialize the decryption engine */
|
||||
if (! EVP_DecryptInit(&d_ctx, EVP_aes_128_cbc(), ikey, ikey+KEY_LEN_BYTES)) {
|
||||
|
@ -435,6 +425,24 @@ static int decrypt_master_key(char *passwd, unsigned char *encrypted_master_key,
|
|||
}
|
||||
}
|
||||
|
||||
static int create_encrypted_random_key(char *passwd, unsigned char *master_key)
|
||||
{
|
||||
int fd;
|
||||
unsigned char buf[KEY_LEN_BYTES];
|
||||
unsigned char ikey[32+32] = { 0 }; /* Big enough to hold a 256 bit key and 256 bit IV */
|
||||
unsigned char salt[32] = { 0 };
|
||||
EVP_CIPHER_CTX e_ctx;
|
||||
int encrypted_len, final_len;
|
||||
|
||||
/* Get some random bits for a key */
|
||||
fd = open("/dev/urandom", O_RDONLY);
|
||||
read(fd, buf, sizeof(buf));
|
||||
close(fd);
|
||||
|
||||
/* Now encrypt it with the password */
|
||||
return encrypt_master_key(passwd, buf, master_key);
|
||||
}
|
||||
|
||||
static int get_orig_mount_parms(char *mount_point, char *fs_type, char *real_blkdev,
|
||||
unsigned long *mnt_flags, char *fs_options)
|
||||
{
|
||||
|
@ -482,6 +490,36 @@ static int wait_and_unmount(char *mountpoint)
|
|||
return rc;
|
||||
}
|
||||
|
||||
#define DATA_PREP_TIMEOUT 100
|
||||
static int prep_data_fs(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
/* Do the prep of the /data filesystem */
|
||||
property_set("vold.post_fs_data_done", "0");
|
||||
property_set("vold.decrypt", "trigger_post_fs_data");
|
||||
SLOGD("Just triggered post_fs_data\n");
|
||||
|
||||
/* Wait a max of 25 seconds, hopefully it takes much less */
|
||||
for (i=0; i<DATA_PREP_TIMEOUT; i++) {
|
||||
char p[16];;
|
||||
|
||||
property_get("vold.post_fs_data_done", p, "0");
|
||||
if (*p == '1') {
|
||||
break;
|
||||
} else {
|
||||
usleep(250000);
|
||||
}
|
||||
}
|
||||
if (i == DATA_PREP_TIMEOUT) {
|
||||
/* Ugh, we failed to prep /data in time. Bail. */
|
||||
return -1;
|
||||
} else {
|
||||
SLOGD("post_fs_data done\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int cryptfs_restart(void)
|
||||
{
|
||||
char fs_type[32];
|
||||
|
@ -491,7 +529,6 @@ int cryptfs_restart(void)
|
|||
unsigned long mnt_flags;
|
||||
struct stat statbuf;
|
||||
int rc = -1, i;
|
||||
#define DATA_PREP_TIMEOUT 100
|
||||
|
||||
/* Here is where we shut down the framework. The init scripts
|
||||
* start all services in one of three classes: core, main or late_start.
|
||||
|
@ -523,31 +560,15 @@ int cryptfs_restart(void)
|
|||
return -1;
|
||||
}
|
||||
|
||||
if (! get_orig_mount_parms("/data", fs_type, real_blkdev, &mnt_flags, fs_options)) {
|
||||
if (! get_orig_mount_parms(DATA_MNT_POINT, fs_type, real_blkdev, &mnt_flags, fs_options)) {
|
||||
SLOGD("Just got orig mount parms\n");
|
||||
|
||||
if (! (rc = wait_and_unmount("/data")) ) {
|
||||
if (! (rc = wait_and_unmount(DATA_MNT_POINT)) ) {
|
||||
/* If that succeeded, then mount the decrypted filesystem */
|
||||
mount(crypto_blkdev, "/data", fs_type, mnt_flags, fs_options);
|
||||
mount(crypto_blkdev, DATA_MNT_POINT, fs_type, mnt_flags, fs_options);
|
||||
|
||||
/* Do the prep of the /data filesystem */
|
||||
property_set("vold.post_fs_data_done", "0");
|
||||
property_set("vold.decrypt", "trigger_post_fs_data");
|
||||
SLOGD("Just triggered post_fs_data\n");
|
||||
|
||||
/* Wait a max of 25 seconds, hopefully it takes much less */
|
||||
for (i=0; i<DATA_PREP_TIMEOUT; i++) {
|
||||
char p[16];;
|
||||
|
||||
property_get("vold.post_fs_data_done", p, "0");
|
||||
if (*p == '1') {
|
||||
break;
|
||||
} else {
|
||||
usleep(250000);
|
||||
}
|
||||
}
|
||||
if (i == DATA_PREP_TIMEOUT) {
|
||||
/* Ugh, we failed to prep /data in time. Bail. */
|
||||
/* Create necessary paths on /data */
|
||||
if (prep_data_fs()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -633,6 +654,12 @@ static int test_mount_encrypted_fs(char *passwd, char *mount_point)
|
|||
* so we can mount it when restarting the framework.
|
||||
*/
|
||||
property_set("ro.crypto.fs_crypto_blkdev", crypto_blkdev);
|
||||
/* Also save a SHA1 of the master key so we can know if we
|
||||
* successfully decrypted the key when we want to change the
|
||||
* password on it.
|
||||
*/
|
||||
SHA1(decrypted_master_key, KEY_LEN_BYTES, saved_key_sha1);
|
||||
key_sha1_saved = 1;
|
||||
rc = 0;
|
||||
}
|
||||
|
||||
|
@ -643,7 +670,7 @@ int cryptfs_check_passwd(char *passwd)
|
|||
{
|
||||
int rc = -1;
|
||||
|
||||
rc = test_mount_encrypted_fs(passwd, "/data");
|
||||
rc = test_mount_encrypted_fs(passwd, DATA_MNT_POINT);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
@ -707,6 +734,7 @@ static int cryptfs_enable_inplace(char *crypto_blkdev, char *real_blkdev, off64_
|
|||
char *buf[CRYPT_INPLACE_BUFSIZE];
|
||||
int rc = -1;
|
||||
off64_t numblocks, i, remainder;
|
||||
off64_t one_pct, cur_pct, new_pct;
|
||||
|
||||
if ( (realfd = open(real_blkdev, O_RDONLY)) < 0) {
|
||||
SLOGE("Error opening real_blkdev %s for inplace encrypt\n", real_blkdev);
|
||||
|
@ -729,11 +757,18 @@ static int cryptfs_enable_inplace(char *crypto_blkdev, char *real_blkdev, off64_
|
|||
|
||||
SLOGE("Encrypting filesystem in place...");
|
||||
|
||||
one_pct = numblocks / 100;
|
||||
cur_pct = 0;
|
||||
/* process the majority of the filesystem in blocks */
|
||||
for (i=0; i<numblocks; i++) {
|
||||
if ( ! (i % 65536)) { //KEN
|
||||
SLOGE("|"); //KEN
|
||||
} //KEN
|
||||
new_pct = i / one_pct;
|
||||
if (new_pct > cur_pct) {
|
||||
char buf[8];
|
||||
|
||||
cur_pct = new_pct;
|
||||
snprintf(buf, sizeof(buf), "%lld", cur_pct);
|
||||
property_set("vold.encrypt_progress", buf);
|
||||
}
|
||||
if (unix_read(realfd, buf, CRYPT_INPLACE_BUFSIZE) <= 0) {
|
||||
SLOGE("Error reading real_blkdev %s for inplace encrypt\n", crypto_blkdev);
|
||||
goto errout;
|
||||
|
@ -756,6 +791,8 @@ static int cryptfs_enable_inplace(char *crypto_blkdev, char *real_blkdev, off64_
|
|||
}
|
||||
}
|
||||
|
||||
property_set("vold.encrypt_progress", "100");
|
||||
|
||||
rc = 0;
|
||||
|
||||
errout:
|
||||
|
@ -767,6 +804,9 @@ errout:
|
|||
|
||||
#define CRYPTO_ENABLE_WIPE 1
|
||||
#define CRYPTO_ENABLE_INPLACE 2
|
||||
|
||||
#define FRAMEWORK_BOOT_WAIT 60
|
||||
|
||||
int cryptfs_enable(char *howarg, char *passwd)
|
||||
{
|
||||
int how = 0;
|
||||
|
@ -774,8 +814,9 @@ int cryptfs_enable(char *howarg, char *passwd)
|
|||
char fs_type[32], fs_options[256], mount_point[32];
|
||||
unsigned long mnt_flags, nr_sec;
|
||||
unsigned char master_key[16], decrypted_master_key[16];
|
||||
int rc, fd;
|
||||
int rc=-1, fd, i;
|
||||
struct crypt_mnt_ftr crypt_ftr;
|
||||
char tmpfs_options[80];
|
||||
|
||||
if (!strcmp(howarg, "wipe")) {
|
||||
how = CRYPTO_ENABLE_WIPE;
|
||||
|
@ -789,7 +830,7 @@ int cryptfs_enable(char *howarg, char *passwd)
|
|||
get_orig_mount_parms(mount_point, fs_type, real_blkdev, &mnt_flags, fs_options);
|
||||
|
||||
/* The init files are setup to stop the class main and late start when
|
||||
* set to 4. They also unmount the fuse filesystem /mnt/sdcard on stingray.
|
||||
* vold sets trigger_shutdown_framework.
|
||||
*/
|
||||
property_set("vold.decrypt", "trigger_shutdown_framework");
|
||||
SLOGD("Just asked init to shut down class main\n");
|
||||
|
@ -799,12 +840,56 @@ int cryptfs_enable(char *howarg, char *passwd)
|
|||
}
|
||||
|
||||
/* Now unmount the /data partition. */
|
||||
if (! (rc = wait_and_unmount("/data")) ) {
|
||||
/* OK, we've unmounted /data, time to setup an encrypted
|
||||
* mapping, and either write a new filesystem or encrypt
|
||||
* in place.
|
||||
*/
|
||||
if (wait_and_unmount(DATA_MNT_POINT)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Do extra work for a better UX when doing the long inplace encryption */
|
||||
if (how == CRYPTO_ENABLE_INPLACE) {
|
||||
/* Now that /data is unmounted, we need to mount a tmpfs
|
||||
* /data, set a property saying we're doing inplace encryption,
|
||||
* and restart the framework.
|
||||
*/
|
||||
property_get("ro.crypto.tmpfs_options", tmpfs_options, "");
|
||||
if (mount("tmpfs", DATA_MNT_POINT, "tmpfs", MS_NOATIME | MS_NOSUID | MS_NODEV,
|
||||
tmpfs_options) < 0) {
|
||||
return -1;
|
||||
}
|
||||
/* Tells the framework that inplace encryption is starting */
|
||||
property_set("vold.encrypt_progress", "startup");
|
||||
|
||||
/* restart the framework. */
|
||||
/* Create necessary paths on /data */
|
||||
if (prep_data_fs()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* startup service classes main and late_start */
|
||||
property_set("vold.decrypt", "trigger_restart_min_framework");
|
||||
SLOGD("Just triggered restart_min_framework\n");
|
||||
|
||||
/* Wait till the framework is ready */
|
||||
for (i=0; i<FRAMEWORK_BOOT_WAIT; i++) {
|
||||
char progress_state[32];
|
||||
|
||||
sleep(1);
|
||||
property_get("vold.encrypt_progress", progress_state, "");
|
||||
if (! strcmp(progress_state, "ready")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == FRAMEWORK_BOOT_WAIT) {
|
||||
/* The framework never rebooted, so abort */
|
||||
return -1;
|
||||
}
|
||||
/* OK, the framework is restarted and displaying a progress bar,
|
||||
* time to setup an encrypted mapping, and either write a new
|
||||
* filesystem or encrypt in place, updating the progress bar
|
||||
* as we work.
|
||||
*/
|
||||
}
|
||||
|
||||
/* Start the actual work of making an encrypted filesystem */
|
||||
fd = open(real_blkdev, O_RDONLY);
|
||||
if ( (nr_sec = get_blkdev_size(fd)) == 0) {
|
||||
SLOGE("Cannot get size of block device %s\n", real_blkdev);
|
||||
|
@ -839,12 +924,58 @@ int cryptfs_enable(char *howarg, char *passwd)
|
|||
return -1;
|
||||
}
|
||||
|
||||
if (! rc) {
|
||||
/* Undo the dm-crypt mapping whether we succeed or not */
|
||||
delete_crypto_blk_dev(crypto_blkdev);
|
||||
|
||||
if (! rc) {
|
||||
/* Success */
|
||||
sleep(2); /* Give the UI a change to show 100% progress */
|
||||
sync();
|
||||
reboot(LINUX_REBOOT_CMD_RESTART);
|
||||
}
|
||||
|
||||
/* Only returns on error */
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cryptfs_changepw(char *oldpw, char *newpw)
|
||||
{
|
||||
struct crypt_mnt_ftr crypt_ftr;
|
||||
unsigned char encrypted_master_key[32], decrypted_master_key[32];
|
||||
unsigned char new_key_sha1[20];
|
||||
char real_blkdev[MAXPATHLEN];
|
||||
|
||||
/* This is only allowed after we've successfully decrypted the master key */
|
||||
if (! key_sha1_saved) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
property_get("ro.crypto.fs_real_blkdev", real_blkdev, "");
|
||||
if (strlen(real_blkdev) == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* get key */
|
||||
if (get_crypt_ftr_and_key(real_blkdev, &crypt_ftr, encrypted_master_key)) {
|
||||
SLOGE("Error getting crypt footer and key\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* decrypt key with old passwd */
|
||||
decrypt_master_key(oldpw, encrypted_master_key, decrypted_master_key);
|
||||
|
||||
/* compute sha1 of decrypted key */
|
||||
SHA1(decrypted_master_key, KEY_LEN_BYTES, new_key_sha1);
|
||||
|
||||
/* If computed sha1 and saved sha1 match, encrypt key with new passwd */
|
||||
if (! memcmp(saved_key_sha1, new_key_sha1, sizeof(saved_key_sha1))) {
|
||||
/* they match, it's safe to re-encrypt the key */
|
||||
encrypt_master_key(newpw, decrypted_master_key, encrypted_master_key);
|
||||
|
||||
/* save the key */
|
||||
put_crypt_ftr_and_key(real_blkdev, &crypt_ftr, encrypted_master_key);
|
||||
} else {
|
||||
SLOGE("SHA1 mismatch");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ extern "C" {
|
|||
int cryptfs_check_passwd(char *pw);
|
||||
int cryptfs_restart(void);
|
||||
int cryptfs_enable(char *flag, char *passwd);
|
||||
int cryptfs_changepw(char *oldpw, char *newpw);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
Loading…
Reference in a new issue