/* * Driver for /dev/crypto device (aka CryptoDev) * * Copyright (c) 2004 Michal Ludvig , 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include MODULE_AUTHOR("Michal Ludvig "); 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 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; init_MUTEX(&ses_new->sem); 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)); init_MUTEX(&fcr->sem); 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 inode *inode, 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, .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);