| /* SPDX-License-Identifier: LGPL-2.1+ */ |
| /*** |
| This file is part of systemd. |
| |
| Copyright 2016 Lennart Poettering |
| |
| systemd is free software; you can redistribute it and/or modify it |
| under the terms of the GNU Lesser General Public License as published by |
| the Free Software Foundation; either version 2.1 of the License, or |
| (at your option) any later version. |
| |
| systemd is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| Lesser General Public License for more details. |
| |
| You should have received a copy of the GNU Lesser General Public License |
| along with systemd; If not, see <http://www.gnu.org/licenses/>. |
| ***/ |
| |
| #include <linux/if_alg.h> |
| #include <stdbool.h> |
| #include <sys/socket.h> |
| |
| #include "alloc-util.h" |
| #include "fd-util.h" |
| #include "hexdecoct.h" |
| #include "khash.h" |
| #include "macro.h" |
| #include "missing.h" |
| #include "string-util.h" |
| #include "util.h" |
| |
| /* On current kernels the maximum digest (according to "grep digestsize /proc/crypto | sort -u") is actually 32, but |
| * let's add some extra room, the few wasted bytes don't really matter... */ |
| #define LONGEST_DIGEST 128 |
| |
| struct khash { |
| int fd; |
| char *algorithm; |
| uint8_t digest[LONGEST_DIGEST+1]; |
| size_t digest_size; |
| bool digest_valid; |
| }; |
| |
| int khash_supported(void) { |
| static const union { |
| struct sockaddr sa; |
| struct sockaddr_alg alg; |
| } sa = { |
| .alg.salg_family = AF_ALG, |
| .alg.salg_type = "hash", |
| .alg.salg_name = "sha256", /* a very common algorithm */ |
| }; |
| |
| static int cached = -1; |
| |
| if (cached < 0) { |
| _cleanup_close_ int fd1 = -1, fd2 = -1; |
| uint8_t buf[LONGEST_DIGEST+1]; |
| |
| fd1 = socket(AF_ALG, SOCK_SEQPACKET|SOCK_CLOEXEC, 0); |
| if (fd1 < 0) { |
| /* The kernel returns EAFNOSUPPORT if AF_ALG is not supported at all */ |
| if (IN_SET(errno, EAFNOSUPPORT, EOPNOTSUPP)) |
| return (cached = false); |
| |
| return -errno; |
| } |
| |
| if (bind(fd1, &sa.sa, sizeof(sa)) < 0) { |
| /* The kernel returns ENOENT if the selected algorithm is not supported at all. We use a check |
| * for SHA256 as a proxy for whether the whole API is supported at all. After all it's one of |
| * the most common hash functions, and if it isn't supported, that's ample indication that |
| * something is really off. */ |
| |
| if (IN_SET(errno, ENOENT, EOPNOTSUPP)) |
| return (cached = false); |
| |
| return -errno; |
| } |
| |
| fd2 = accept4(fd1, NULL, 0, SOCK_CLOEXEC); |
| if (fd2 < 0) { |
| if (errno == EOPNOTSUPP) |
| return (cached = false); |
| |
| return -errno; |
| } |
| |
| if (recv(fd2, buf, sizeof(buf), 0) < 0) { |
| /* On some kernels we get ENOKEY for non-keyed hash functions (such as sha256), let's refuse |
| * using the API in those cases, since the kernel is |
| * broken. https://github.com/systemd/systemd/issues/8278 */ |
| |
| if (IN_SET(errno, ENOKEY, EOPNOTSUPP)) |
| return (cached = false); |
| } |
| |
| cached = true; |
| } |
| |
| return cached; |
| } |
| |
| int khash_new_with_key(khash **ret, const char *algorithm, const void *key, size_t key_size) { |
| union { |
| struct sockaddr sa; |
| struct sockaddr_alg alg; |
| } sa = { |
| .alg.salg_family = AF_ALG, |
| .alg.salg_type = "hash", |
| }; |
| |
| _cleanup_(khash_unrefp) khash *h = NULL; |
| _cleanup_close_ int fd = -1; |
| int supported; |
| ssize_t n; |
| |
| assert(ret); |
| assert(key || key_size == 0); |
| |
| /* Filter out an empty algorithm early, as we do not support an algorithm by that name. */ |
| if (isempty(algorithm)) |
| return -EINVAL; |
| |
| /* Overly long hash algorithm names we definitely do not support */ |
| if (strlen(algorithm) >= sizeof(sa.alg.salg_name)) |
| return -EOPNOTSUPP; |
| |
| supported = khash_supported(); |
| if (supported < 0) |
| return supported; |
| if (supported == 0) |
| return -EOPNOTSUPP; |
| |
| fd = socket(AF_ALG, SOCK_SEQPACKET|SOCK_CLOEXEC, 0); |
| if (fd < 0) |
| return -errno; |
| |
| strcpy((char*) sa.alg.salg_name, algorithm); |
| if (bind(fd, &sa.sa, sizeof(sa)) < 0) { |
| if (errno == ENOENT) |
| return -EOPNOTSUPP; |
| return -errno; |
| } |
| |
| if (key) { |
| if (setsockopt(fd, SOL_ALG, ALG_SET_KEY, key, key_size) < 0) |
| return -errno; |
| } |
| |
| h = new0(khash, 1); |
| if (!h) |
| return -ENOMEM; |
| |
| h->fd = accept4(fd, NULL, 0, SOCK_CLOEXEC); |
| if (h->fd < 0) |
| return -errno; |
| |
| h->algorithm = strdup(algorithm); |
| if (!h->algorithm) |
| return -ENOMEM; |
| |
| /* Temporary fix for rc kernel bug: https://bugzilla.redhat.com/show_bug.cgi?id=1395896 */ |
| (void) send(h->fd, NULL, 0, 0); |
| |
| /* Figure out the digest size */ |
| n = recv(h->fd, h->digest, sizeof(h->digest), 0); |
| if (n < 0) |
| return -errno; |
| if (n >= LONGEST_DIGEST) /* longer than what we expected? If so, we don't support this */ |
| return -EOPNOTSUPP; |
| |
| h->digest_size = (size_t) n; |
| h->digest_valid = true; |
| |
| /* Temporary fix for rc kernel bug: https://bugzilla.redhat.com/show_bug.cgi?id=1395896 */ |
| (void) send(h->fd, NULL, 0, 0); |
| |
| *ret = h; |
| h = NULL; |
| |
| return 0; |
| } |
| |
| int khash_new(khash **ret, const char *algorithm) { |
| return khash_new_with_key(ret, algorithm, NULL, 0); |
| } |
| |
| khash* khash_unref(khash *h) { |
| if (!h) |
| return NULL; |
| |
| safe_close(h->fd); |
| free(h->algorithm); |
| return mfree(h); |
| } |
| |
| int khash_dup(khash *h, khash **ret) { |
| _cleanup_(khash_unrefp) khash *copy = NULL; |
| |
| assert(h); |
| assert(ret); |
| |
| copy = newdup(khash, h, 1); |
| if (!copy) |
| return -ENOMEM; |
| |
| copy->fd = -1; |
| copy->algorithm = strdup(h->algorithm); |
| if (!copy->algorithm) |
| return -ENOMEM; |
| |
| copy->fd = accept4(h->fd, NULL, 0, SOCK_CLOEXEC); |
| if (copy->fd < 0) |
| return -errno; |
| |
| *ret = copy; |
| copy = NULL; |
| |
| return 0; |
| } |
| |
| const char *khash_get_algorithm(khash *h) { |
| assert(h); |
| |
| return h->algorithm; |
| } |
| |
| size_t khash_get_size(khash *h) { |
| assert(h); |
| |
| return h->digest_size; |
| } |
| |
| int khash_reset(khash *h) { |
| ssize_t n; |
| |
| assert(h); |
| |
| n = send(h->fd, NULL, 0, 0); |
| if (n < 0) |
| return -errno; |
| |
| h->digest_valid = false; |
| |
| return 0; |
| } |
| |
| int khash_put(khash *h, const void *buffer, size_t size) { |
| ssize_t n; |
| |
| assert(h); |
| assert(buffer || size == 0); |
| |
| if (size <= 0) |
| return 0; |
| |
| n = send(h->fd, buffer, size, MSG_MORE); |
| if (n < 0) |
| return -errno; |
| |
| h->digest_valid = false; |
| |
| return 0; |
| } |
| |
| int khash_put_iovec(khash *h, const struct iovec *iovec, size_t n) { |
| struct msghdr mh = { |
| mh.msg_iov = (struct iovec*) iovec, |
| mh.msg_iovlen = n, |
| }; |
| ssize_t k; |
| |
| assert(h); |
| assert(iovec || n == 0); |
| |
| if (n <= 0) |
| return 0; |
| |
| k = sendmsg(h->fd, &mh, MSG_MORE); |
| if (k < 0) |
| return -errno; |
| |
| h->digest_valid = false; |
| |
| return 0; |
| } |
| |
| static int retrieve_digest(khash *h) { |
| ssize_t n; |
| |
| assert(h); |
| |
| if (h->digest_valid) |
| return 0; |
| |
| n = recv(h->fd, h->digest, h->digest_size, 0); |
| if (n < 0) |
| return n; |
| if ((size_t) n != h->digest_size) /* digest size changed? */ |
| return -EIO; |
| |
| h->digest_valid = true; |
| |
| return 0; |
| } |
| |
| int khash_digest_data(khash *h, const void **ret) { |
| int r; |
| |
| assert(h); |
| assert(ret); |
| |
| r = retrieve_digest(h); |
| if (r < 0) |
| return r; |
| |
| *ret = h->digest; |
| return 0; |
| } |
| |
| int khash_digest_string(khash *h, char **ret) { |
| int r; |
| char *p; |
| |
| assert(h); |
| assert(ret); |
| |
| r = retrieve_digest(h); |
| if (r < 0) |
| return r; |
| |
| p = hexmem(h->digest, h->digest_size); |
| if (!p) |
| return -ENOMEM; |
| |
| *ret = p; |
| return 0; |
| } |