Wednesday, September 27, 2017

SMB3 encryption in the kernel cifs module

This is a code walk through which takes you through how the cifs module goes through encrypting its communications with the server.

Mount option parsing.

static int
cifs_parse_mount_options(const char *mountdata, const char *devname,
                         struct smb_vol *vol)
{
..
                case Opt_seal:
                        /* we do not do the following in secFlags because seal
                         * is a per tree connection (mount) not a per socket
                         * or per-smb connection option in the protocol
                         * vol->secFlg |= CIFSSEC_MUST_SEAL;
                         */
                        vol->seal = 1;
..
}

static struct cifs_tcon *
cifs_get_tcon(struct cifs_ses *ses, struct smb_vol *volume_info)
{
..
        if (volume_info->seal) {
                if (ses->server->vals->protocol_id == 0) {
                        cifs_dbg(VFS,
                                 "SMB3 or later required for encryption\n");
                        rc = -EOPNOTSUPP;
                        goto out_fail;
#ifdef CONFIG_CIFS_SMB2
                } else if (tcon->ses->server->capabilities &
                                        SMB2_GLOBAL_CAP_ENCRYPTION)
                        tcon->seal = true;
                else {
                        cifs_dbg(VFS, "Encryption is not supported on share\n");
                        rc = -EOPNOTSUPP;
                        goto out_fail;
#endif /* CONFIG_CIFS_SMB2 */
                }
        }
..
}

So at this point, tcon->seal is set.

We check if encryption is required before making a call using the check
static int encryption_required(const struct cifs_tcon *tcon)
{
        if (!tcon)
                return 0;
        if ((tcon->ses->session_flags & SMB2_SESSION_FLAG_ENCRYPT_DATA) ||
            (tcon->share_flags & SHI1005_FLAGS_ENCRYPT_DATA))
                return 1;
        if (tcon->seal &&
            (tcon->ses->server->capabilities & SMB2_GLOBAL_CAP_ENCRYPTION))
                return 1;
        return 0;
}

Which is then called before each SMB call to determine if encryption is required.

int
SMB2_open(const unsigned int xid, struct cifs_open_parms *oparms, __le16 *path,
          __u8 *oplock, struct smb2_file_all_info *buf,
          struct smb2_err_rsp **err_buf)
{
..
        if (encryption_required(tcon))
                flags |= CIFS_TRANSFORM_REQ;
..
}

This check for encryption is made for every call and if required, the CIFS_TRANSFORM_REQ flag is set.

struct smb2_transform_hdr {
        __be32 smb2_buf_length; /* big endian on wire */
                                /* length is only two or three bytes - with
                                 one or two byte type preceding it that MBZ */
        __le32 ProtocolId;      /* 0xFD 'S' 'M' 'B' */
        __u8   Signature[16];
//Below this point, we use the information to authenticate but not encrypt. Only the data is encrypted.
//This is the associate data.

        __u8   Nonce[16];
        __le32 OriginalMessageSize;
        __u16  Reserved1;
        __le16 Flags; /* EncryptionAlgorithm */
        __u64  SessionId;
} __packed;


static int
smb_send_rqst(struct TCP_Server_Info *server, struct smb_rqst *rqst, int flags)
{
        struct smb_rqst cur_rqst;
        int rc;
        // If the packet doesn't need to be transformed, send it out
        if (!(flags & CIFS_TRANSFORM_REQ))
                return __smb_send_rqst(server, rqst);
        //Check for init_transform_rq and free_transform_rq to transform headers.        if (!server->ops->init_transform_rq ||
            !server->ops->free_transform_rq) {
                cifs_dbg(VFS, "Encryption requested but transform callbacks are missed\n");
                return -EIO;
        }
        //Call the transform functions.
        //Here cur request is a pointer to a cur_rqst on the heap.

        rc = server->ops->init_transform_rq(server, &cur_rqst, rqst);
        if (rc)
                return rc;
        //Send packets.        rc = __smb_send_rqst(server, &cur_rqst);
        //Free packets.        server->ops->free_transform_rq(&cur_rqst);
        return rc;
}

struct smb_version_operations smb30_operations = {
..
        .init_transform_rq = smb3_init_transform_rq,
        .free_transform_rq = smb3_free_transform_rq,
..
};
static int
smb3_init_transform_rq(struct TCP_Server_Info *server, struct smb_rqst *new_rq,
                       struct smb_rqst *old_rq)
{
        struct kvec *iov;
        struct page **pages;
        struct smb2_transform_hdr *tr_hdr;
        unsigned int npages = old_rq->rq_npages;
        int i;
        int rc = -ENOMEM;
        //Allocate an array of page pointers        pages = kmalloc_array(npages, sizeof(struct page *), GFP_KERNEL);
        if (!pages)
                return rc;
        //Copy over the data for pages from the old array.
        new_rq->rq_pages = pages;
        new_rq->rq_npages = old_rq->rq_npages;
        new_rq->rq_pagesz = old_rq->rq_pagesz;
        new_rq->rq_tailsz = old_rq->rq_tailsz;
        //Allocate a new set of pages for the new request to be transformed.
        for (i = 0; i < npages; i++) {
                pages[i] = alloc_page(GFP_KERNEL|__GFP_HIGHMEM);
                if (!pages[i])
                        goto err_free_pages;
        }
        //Create a new array of iov pointers.
        iov = kmalloc_array(old_rq->rq_nvec, sizeof(struct kvec), GFP_KERNEL);
        if (!iov)
                goto err_free_pages;
        /* copy all iovs from the old except the 1st one (rfc1002 length) */
        memcpy(&iov[1], &old_rq->rq_iov[1],
                                sizeof(struct kvec) * (old_rq->rq_nvec - 1));
        new_rq->rq_iov = iov;
        new_rq->rq_nvec = old_rq->rq_nvec;
        //Add a transform header to be used for the first iov.
        tr_hdr = kmalloc(sizeof(struct smb2_transform_hdr), GFP_KERNEL);
        if (!tr_hdr)
                goto err_free_iov;
        /* fill the 1st iov with a transform header */
        fill_transform_hdr(tr_hdr, old_rq);
        new_rq->rq_iov[0].iov_base = tr_hdr;
        new_rq->rq_iov[0].iov_len = sizeof(struct smb2_transform_hdr);
        /* copy pages form the old */
        for (i = 0; i < npages; i++) {
                char *dst = kmap(new_rq->rq_pages[i]);
                char *src = kmap(old_rq->rq_pages[i]);
                unsigned int len = (i < npages - 1) ? new_rq->rq_pagesz :
                                                        new_rq->rq_tailsz;
                memcpy(dst, src, len);
                kunmap(new_rq->rq_pages[i]);
                kunmap(old_rq->rq_pages[i]);
        }
        //Now encrypt the new iov.
        rc = crypt_message(server, new_rq, 1);
        cifs_dbg(FYI, "encrypt message returned %d", rc);
        if (rc)
                goto err_free_tr_hdr;
        return rc;
err_free_tr_hdr:
        kfree(tr_hdr);
err_free_iov:
        kfree(iov);
err_free_pages:
        for (i = i - 1; i >= 0; i--)
                put_page(pages[i]);
        kfree(pages);
        return rc;
}

static int
crypt_message(struct TCP_Server_Info *server, struct smb_rqst *rqst, int enc)
{
        struct smb2_transform_hdr *tr_hdr =
                        (struct smb2_transform_hdr *)rqst->rq_iov[0].iov_base;
        unsigned int assoc_data_len = sizeof(struct smb2_transform_hdr) - 24;
        int rc = 0;
        struct scatterlist *sg;
        u8 sign[SMB2_SIGNATURE_SIZE] = {};
        u8 key[SMB3_SIGN_KEY_SIZE];
        struct aead_request *req;
        char *iv;
        unsigned int iv_len;
        struct cifs_crypt_result result = {0, };
        struct crypto_aead *tfm;
        unsigned int crypt_len = le32_to_cpu(tr_hdr->OriginalMessageSize);
        init_completion(&result.completion);
        //Go through the sessions list and get the encryption/decryption key
        rc = smb2_get_enc_key(server, tr_hdr->SessionId, enc, key);
        if (rc) {
                cifs_dbg(VFS, "%s: Could not get %scryption key\n", __func__,
                         enc ? "en" : "de");
                return 0;
        }
        //Allocate the AED transforms and save it in server->secmech.ccmaesencrypt/decrypt
        //This is done only once for the server
        rc = smb3_crypto_aead_allocate(server);
        if (rc) {
                cifs_dbg(VFS, "%s: crypto alloc failed\n", __func__);
                return rc;
        }
       //use the right tfm
        tfm = enc ? server->secmech.ccmaesencrypt :
                                                server->secmech.ccmaesdecrypt;
        //Set the session key
        rc = crypto_aead_setkey(tfm, key, SMB3_SIGN_KEY_SIZE);
        if (rc) {
                cifs_dbg(VFS, "%s: Failed to set aead key %d\n", __func__, rc);
                return rc;
        }
        rc = crypto_aead_setauthsize(tfm, SMB2_SIGNATURE_SIZE);
        if (rc) {
                cifs_dbg(VFS, "%s: Failed to set authsize %d\n", __func__, rc);
                return rc;
        }
        req = aead_request_alloc(tfm, GFP_KERNEL);
        if (!req) {
                cifs_dbg(VFS, "%s: Failed to alloc aead request", __func__);
                return -ENOMEM;
        }
        if (!enc) {
                memcpy(sign, &tr_hdr->Signature, SMB2_SIGNATURE_SIZE);
                crypt_len += SMB2_SIGNATURE_SIZE;
        }
        sg = init_sg(rqst, sign);
        if (!sg) {
                cifs_dbg(VFS, "%s: Failed to init sg %d", __func__, rc);
                goto free_req;
        }
        iv_len = crypto_aead_ivsize(tfm);
         iv = kzalloc(iv_len, GFP_KERNEL);
        if (!iv) {
                cifs_dbg(VFS, "%s: Failed to alloc IV", __func__);
                goto free_sg;
        }
        iv[0] = 3;
        memcpy(iv + 1, (char *)tr_hdr->Nonce, SMB3_AES128CMM_NONCE);
        aead_request_set_crypt(req, sg, sg, crypt_len, iv);
        aead_request_set_ad(req, assoc_data_len);
        aead_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
                                  cifs_crypt_complete, &result);
        rc = enc ? crypto_aead_encrypt(req) : crypto_aead_decrypt(req);
        if (rc == -EINPROGRESS || rc == -EBUSY) {
                wait_for_completion(&result.completion);
                rc = result.err;
        }
        if (!rc && enc)
                memcpy(&tr_hdr->Signature, sign, SMB2_SIGNATURE_SIZE);
        kfree(iv);
free_sg:
        kfree(sg);
free_req:
        kfree(req);
        return rc;
}
                                            

Setting a frost alarm on Home assistant

One of the issues with winter is the possibility of ice covering your windscreen which needs to be cleared before you can drive. Clearing ou...