diff options
Diffstat (limited to 'crypto/cryptodev.c')
-rw-r--r-- | crypto/cryptodev.c | 558 |
1 files changed, 558 insertions, 0 deletions
diff --git a/crypto/cryptodev.c b/crypto/cryptodev.c new file mode 100644 index 00000000..5f86e8fa --- /dev/null +++ b/crypto/cryptodev.c @@ -0,0 +1,558 @@ +/* + * Driver for /dev/crypto device (aka CryptoDev) + * + * Copyright (c) 2004 Michal Ludvig <mludvig@suse.cz>, SuSE Labs + * + * Device /dev/crypto provides an interface for + * accessing kernel CryptoAPI algorithms (ciphers, + * hashes) from userspace programs. + * + * /dev/crypto interface was originally introduced in + * OpenBSD and this module attempts to keep the API, + * although a bit extended. + * + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/init.h> +#include <linux/sched.h> +#include <linux/fs.h> +#include <linux/file.h> +#include <linux/miscdevice.h> +#include <linux/crypto.h> +#include <linux/mm.h> +#include <linux/highmem.h> +#include <linux/random.h> +#include <linux/cryptodev.h> +#include <asm/uaccess.h> +#include <asm/ioctl.h> +#include <linux/scatterlist.h> + +MODULE_AUTHOR("Michal Ludvig <mludvig@suse.cz>"); +MODULE_DESCRIPTION("CryptoDev driver"); +MODULE_LICENSE("Dual BSD/GPL"); + +/* ====== Compile-time config ====== */ + +#define CRYPTODEV_STATS + +/* ====== Module parameters ====== */ + +static int verbosity = 0; +module_param(verbosity, int, 0644); +MODULE_PARM_DESC(verbosity, "0: normal, 1: verbose, 2: debug"); + +#ifdef CRYPTODEV_STATS +static int enable_stats = 0; +module_param(enable_stats, int, 0644); +MODULE_PARM_DESC(enable_stats, "collect statictics about cryptodev usage"); +#endif + +/* ====== Debug helpers ====== */ + +#define PFX "cryptodev: " +#define dprintk(level,severity,format,a...) \ + do { \ + if (level <= verbosity) \ + printk(severity PFX "%s[%u]: " format, \ + current->comm, current->pid, \ + ##a); \ + } while (0) + +/* ====== CryptoAPI ====== */ + +struct csession { + struct list_head entry; + struct semaphore sem; + struct crypto_blkcipher *tfm; + uint32_t sid; +#ifdef CRYPTODEV_STATS +#if ! ((COP_ENCRYPT < 2) && (COP_DECRYPT < 2)) +#error Struct csession.stat uses COP_{ENCRYPT,DECRYPT} as indices. Do something! +#endif + unsigned long long stat[2]; + size_t stat_max_size, stat_count; +#endif +}; + +struct fcrypt { + struct list_head list; + struct semaphore sem; +}; + +/* Prepare session for future use. */ +static int +crypto_create_session(struct fcrypt *fcr, struct session_op *sop) +{ + struct csession *ses_new, *ses_ptr; + struct crypto_blkcipher *tfm; + int ret = 0; + char alg_name[MAX_ALG_NAME_LEN+1]; + char alg_full_name[MAX_ALG_NAME_LEN+1]; + const char *mode; + u8 *keyp; + + /* Does the request make sense? */ + if (!sop->cipher == !sop->mac) { + dprintk(1, KERN_DEBUG, "Both 'cipher' and 'mac' set or unset.\n"); + return -EINVAL; + } + + /* Copy-in the algorithm name if necessary. */ + if (!sop->alg_namelen) { + /* Hmm, compatibility with OpenBSD CRYPTO_* constants... + Should we support it? */ + dprintk(2, KERN_DEBUG, "OpenBSD constants are not (yet?) supported.\n"); + return -EINVAL; + } + + if(sop->alg_namelen > MAX_ALG_NAME_LEN) { + dprintk(1, KERN_DEBUG, "Algorithm name too long (%zu > %u)\n", + sop->alg_namelen, MAX_ALG_NAME_LEN); + return -EINVAL; + } + + copy_from_user(alg_name, sop->alg_name, sop->alg_namelen); + alg_name[sop->alg_namelen] = '\0'; + + if(!sop->cipher) { + dprintk(2, KERN_DEBUG, "Hashes are not yet supported.\n"); + return -EINVAL; + } + + switch (sop->cipher & CRYPTO_FLAG_MASK) { + case CRYPTO_FLAG_ECB: mode = "ebc"; break; + case CRYPTO_FLAG_CBC: mode = "cbc"; break; + case CRYPTO_FLAG_CFB: mode = "cfb"; break; + case CRYPTO_FLAG_CTR: mode = "ctr"; break; +#if 0 + /* These modes are not yet supported. */ + case CRYPTO_FLAG_OFB: mode = "ofb"; break; +#endif + default: return -EINVAL; + } + snprintf(alg_full_name, sizeof(alg_full_name) - 1, "%s(%s)", mode, alg_name); + + /* Set-up crypto transform. */ + tfm = crypto_alloc_blkcipher(alg_full_name, 0, 0); + if (!tfm) { + dprintk(1, KERN_DEBUG, "Failed to load transform for %s %s\n", + alg_name, mode); + return -EINVAL; + } + +#if 0 + /* Was correct key length supplied? */ + if ((sop->keylen < crypto_tfm_alg_min_keysize(tfm)) || + (sop->keylen > crypto_tfm_alg_max_keysize(tfm))) { + dprintk(1, KERN_DEBUG, + "Wrong keylen '%zu' for algorithm '%s'. Use %u to %u.\n", + sop->keylen, alg_name, crypto_tfm_alg_min_keysize(tfm), + crypto_tfm_alg_max_keysize(tfm)); + crypto_free_blkcipher(tfm); + return -EINVAL; + } +#endif + + /* Copy the key from user and set to TFM. */ + keyp = kmalloc(sop->keylen, GFP_KERNEL); + if (keyp == NULL) { + dprintk(1, KERN_ERR, + "Unable to allocate key buffer.\n"); + crypto_free_blkcipher(tfm); + return -ENOMEM; + } + copy_from_user(keyp, sop->key, sop->keylen); + ret = crypto_blkcipher_setkey(tfm, keyp, sop->keylen); + kfree(keyp); + if (ret) { + dprintk(2, KERN_DEBUG, + "Setting key failed for %s-%zu-%s: flags=0x%X\n", + alg_name, sop->keylen*8, mode, crypto_blkcipher_tfm(tfm)->crt_flags); + dprintk(2, KERN_DEBUG, + "(see CRYPTO_TFM_RES_* in <linux/crypto.h> for details)\n"); + crypto_free_blkcipher(tfm); + return -EINVAL; + } + + /* Create a session and put it to the list. */ + ses_new = kmalloc(sizeof(*ses_new), GFP_KERNEL); + if(!ses_new) + return -ENOMEM; + + memset(ses_new, 0, sizeof(*ses_new)); + get_random_bytes(&ses_new->sid, sizeof(ses_new->sid)); + ses_new->tfm = tfm; + sema_init(&ses_new->sem, 1); + + down(&fcr->sem); +restart: + list_for_each_entry(ses_ptr, &fcr->list, entry) { + /* Check for duplicate SID */ + if (unlikely(ses_new->sid == ses_ptr->sid)) { + get_random_bytes(&ses_new->sid, sizeof(ses_new->sid)); + /* Unless we have a broken RNG this + shouldn't loop forever... ;-) */ + goto restart; + } + } + + list_add(&ses_new->entry, &fcr->list); + up(&fcr->sem); + + dprintk(2, KERN_DEBUG, "Added session 0x%08X (%s-%zu-%s)\n", + ses_new->sid, alg_name, sop->keylen*8, mode); + + /* Fill in some values for the user. */ + sop->ses = ses_new->sid; + sop->blocksize = crypto_blkcipher_blocksize(tfm); + + return 0; +} + +/* Everything that needs to be done when remowing a session. */ +static inline void +crypto_destroy_session(struct csession *ses_ptr) +{ + if(down_trylock(&ses_ptr->sem)) { + dprintk(2, KERN_DEBUG, "Waiting for semaphore of sid=0x%08X\n", + ses_ptr->sid); + down(&ses_ptr->sem); + } + dprintk(2, KERN_DEBUG, "Removed session 0x%08X\n", ses_ptr->sid); +#if defined(CRYPTODEV_STATS) + if(enable_stats) + dprintk(2, KERN_DEBUG, + "Usage in Bytes: enc=%llu, dec=%llu, max=%zu, avg=%lu, cnt=%zu\n", + ses_ptr->stat[COP_ENCRYPT], ses_ptr->stat[COP_DECRYPT], + ses_ptr->stat_max_size, ses_ptr->stat_count > 0 + ? ((unsigned long)(ses_ptr->stat[COP_ENCRYPT]+ + ses_ptr->stat[COP_DECRYPT]) / + ses_ptr->stat_count) : 0, + ses_ptr->stat_count); +#endif + crypto_free_blkcipher(ses_ptr->tfm); + ses_ptr->tfm = NULL; + up(&ses_ptr->sem); + kfree(ses_ptr); +} + +/* Look up a session by ID and remove. */ +static int +crypto_finish_session(struct fcrypt *fcr, uint32_t sid) +{ + struct csession *tmp, *ses_ptr; + struct list_head *head; + int ret = 0; + + down(&fcr->sem); + head = &fcr->list; + list_for_each_entry_safe(ses_ptr, tmp, head, entry) { + if(ses_ptr->sid == sid) { + list_del(&ses_ptr->entry); + crypto_destroy_session(ses_ptr); + break; + } + } + + if (!ses_ptr) { + dprintk(1, KERN_ERR, "Session with sid=0x%08X not found!\n", sid); + ret = -ENOENT; + } + up(&fcr->sem); + + return ret; +} + +/* Remove all sessions when closing the file */ +static int +crypto_finish_all_sessions(struct fcrypt *fcr) +{ + struct csession *tmp, *ses_ptr; + + down(&fcr->sem); + list_for_each_entry_safe(ses_ptr, tmp, &fcr->list, entry) { + list_del(&ses_ptr->entry); + crypto_destroy_session(ses_ptr); + } + up(&fcr->sem); + + return 0; +} + +/* Look up session by session ID. The returned session is locked. */ +static struct csession * +crypto_get_session_by_sid(struct fcrypt *fcr, uint32_t sid) +{ + struct csession *ses_ptr; + + down(&fcr->sem); + list_for_each_entry(ses_ptr, &fcr->list, entry) { + if(ses_ptr->sid == sid) { + down(&ses_ptr->sem); + break; + } + } + up(&fcr->sem); + + return ses_ptr; +} + +/* This is the main crypto function - feed it with plaintext + and get a ciphertext (or vice versa :-) */ +static int +crypto_run(struct fcrypt *fcr, struct crypt_op *cop) +{ + char *data, *ivp; + char __user *src, __user *dst; + struct scatterlist sg; + struct csession *ses_ptr; + unsigned int ivsize; + size_t nbytes, bufsize; + int ret = 0; + struct blkcipher_desc desc; + + nbytes = cop->len; + + if (cop->op != COP_ENCRYPT && cop->op != COP_DECRYPT) { + dprintk(1, KERN_DEBUG, "invalid operation op=%u\n", cop->op); + return -EINVAL; + } + + ses_ptr = crypto_get_session_by_sid(fcr, cop->ses); + if (!ses_ptr) { + dprintk(1, KERN_ERR, "invalid session ID=0x%08X\n", cop->ses); + return -EINVAL; + } + + if (nbytes % crypto_blkcipher_blocksize(ses_ptr->tfm)) { + dprintk(1, KERN_ERR, + "data size (%zu) isn't a multiple of block size (%u)\n", + nbytes, crypto_blkcipher_blocksize(ses_ptr->tfm)); + ret = -EINVAL; + goto out_unlock; + } + + bufsize = PAGE_SIZE < nbytes ? PAGE_SIZE : nbytes; + data = (char*)__get_free_page(GFP_KERNEL); + + if (unlikely(!data)) { + ret = -ENOMEM; + goto out_unlock; + } + + ivsize = crypto_blkcipher_ivsize(ses_ptr->tfm); + + ivp = kmalloc(ivsize, GFP_KERNEL); + if (unlikely(!ivp)) { + free_page((unsigned long)data); + ret = -ENOMEM; + goto out_unlock; + } + + if (cop->iv) { + copy_from_user(ivp, cop->iv, ivsize); + crypto_blkcipher_set_iv(ses_ptr->tfm, ivp, ivsize); + } + + src = cop->src; + dst = cop->dst; + + desc.tfm = ses_ptr->tfm; + desc.info = NULL; + desc.flags = 0; + + while (nbytes > 0) { + size_t current_len = nbytes > bufsize ? bufsize : nbytes; + + copy_from_user(data, src, current_len); + + sg_set_buf(&sg, data, current_len); + + if (cop->op == COP_DECRYPT) + ret = crypto_blkcipher_decrypt(&desc, &sg, &sg, current_len); + else + ret = crypto_blkcipher_encrypt(&desc, &sg, &sg, current_len); + + if (unlikely(ret)) { + dprintk(0, KERN_ERR, "CryptoAPI failure: flags=0x%x\n", + crypto_blkcipher_tfm(ses_ptr->tfm)->crt_flags); + goto out; + } + + copy_to_user(dst, data, current_len); + + nbytes -= current_len; + src += current_len; + dst += current_len; + } + +#if defined(CRYPTODEV_STATS) + if (enable_stats) { + /* this is safe - we check cop->op at the function entry */ + ses_ptr->stat[cop->op] += cop->len; + if (ses_ptr->stat_max_size < cop->len) + ses_ptr->stat_max_size = cop->len; + ses_ptr->stat_count++; + } +#endif + +out: + free_page((unsigned long)data); + + kfree(ivp); + +out_unlock: + up(&ses_ptr->sem); + + return ret; +} + +/* ====== /dev/crypto ====== */ + +static int +cryptodev_open(struct inode *inode, struct file *filp) +{ + struct fcrypt *fcr; + + fcr = kmalloc(sizeof(*fcr), GFP_KERNEL); + if(!fcr) + return -ENOMEM; + + memset(fcr, 0, sizeof(*fcr)); + sema_init(&fcr->sem, 1); + INIT_LIST_HEAD(&fcr->list); + filp->private_data = fcr; + + return 0; +} + +static int +cryptodev_release(struct inode *inode, struct file *filp) +{ + struct fcrypt *fcr = filp->private_data; + + if(fcr) { + crypto_finish_all_sessions(fcr); + kfree(fcr); + filp->private_data = NULL; + } + return 0; +} + +static int +clonefd(struct file *filp) +{ + int fd; + + fd = get_unused_fd(); + if (fd >= 0) { + get_file(filp); + fd_install(fd, filp); + } + + return fd; +} + +static int +cryptodev_ioctl(struct file *filp, + unsigned int cmd, unsigned long arg) +{ + struct session_op sop; + struct crypt_op cop; + struct fcrypt *fcr = filp->private_data; + uint32_t ses; + int ret, fd; + + if (!fcr) + BUG(); + + switch (cmd) { + case CRIOGET: + fd = clonefd(filp); + put_user(fd, (int*)arg); + return 0; + + case CIOCGSESSION: + copy_from_user(&sop, (void*)arg, sizeof(sop)); + ret = crypto_create_session(fcr, &sop); + if (ret) + return ret; + copy_to_user((void*)arg, &sop, sizeof(sop)); + return 0; + + case CIOCFSESSION: + get_user(ses, (uint32_t*)arg); + ret = crypto_finish_session(fcr, ses); + return ret; + + case CIOCCRYPT: + copy_from_user(&cop, (void*)arg, sizeof(cop)); + ret = crypto_run(fcr, &cop); + copy_to_user((void*)arg, &cop, sizeof(cop)); + return ret; + + default: + return -EINVAL; + } +} + +struct file_operations cryptodev_fops = { + .owner = THIS_MODULE, + .open = cryptodev_open, + .release = cryptodev_release, + .unlocked_ioctl = cryptodev_ioctl, +}; + +struct miscdevice cryptodev = { + .minor = CRYPTODEV_MINOR, + .name = "crypto", + .fops = &cryptodev_fops, +}; + +static int +cryptodev_register(void) +{ + int rc; + + rc = misc_register (&cryptodev); + if (rc) { + printk(KERN_ERR PFX "registeration of /dev/crypto failed\n"); + return rc; + } + + return 0; +} + +static void +cryptodev_deregister(void) +{ + misc_deregister(&cryptodev); +} + +/* ====== Module init/exit ====== */ + +int __init +init_cryptodev(void) +{ + int rc; + + rc = cryptodev_register(); + if (rc) + return rc; + + printk(KERN_INFO PFX "driver loaded.\n"); + + return 0; +} + +void __exit +exit_cryptodev(void) +{ + cryptodev_deregister(); + printk(KERN_INFO PFX "driver unloaded.\n"); +} + +module_init(init_cryptodev); +module_exit(exit_cryptodev); |