/* af_alg.c - Compute message digests from file streams and buffers.
   Copyright (C) 2018-2019 Free Software Foundation, Inc.

   This program is free software; you can redistribute it and/or modify it
   under the terms of the GNU General Public License as published by the
   Free Software Foundation; either version 3, or (at your option) any
   later version.

   This program 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, see <https://www.gnu.org/licenses/>.  */

/* Written by Matteo Croce <mcroce@redhat.com>, 2018.  */

#include <config.h>

#if USE_LINUX_CRYPTO_API

#include "af_alg.h"

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <linux/if_alg.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <sys/socket.h>

#include "sys-limits.h"

#define BLOCKSIZE 32768

/* Return a newly created socket for ALG.
   On error, return a negative error number.  */
static int
alg_socket (char const *alg)
{
  struct sockaddr_alg salg = {
    .salg_family = AF_ALG,
    .salg_type = "hash",
  };
  /* Copy alg into salg.salg_name, without calling strcpy nor strlen.  */
  for (size_t i = 0; (salg.salg_name[i] = alg[i]) != '\0'; i++)
    if (i == sizeof salg.salg_name - 1)
      /* alg is too long.  */
      return -EINVAL;

  int cfd = socket (AF_ALG, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
  if (cfd < 0)
    return -EAFNOSUPPORT;
  int ofd = (bind (cfd, (struct sockaddr *) &salg, sizeof salg) == 0
             ? accept4 (cfd, NULL, 0, SOCK_CLOEXEC)
             : -1);
  close (cfd);
  return ofd < 0 ? -EAFNOSUPPORT : ofd;
}

int
afalg_buffer (const char *buffer, size_t len, const char *alg,
              void *resblock, ssize_t hashlen)
{
  /* On Linux < 4.9, the value for an empty stream is wrong (all zeroes).
     See <https://patchwork.kernel.org/patch/9308641/>.
     This was not fixed properly until November 2016,
     see <https://patchwork.kernel.org/patch/9434741/>.  */
  if (len == 0)
    return -EAFNOSUPPORT;

  int ofd = alg_socket (alg);
  if (ofd < 0)
    return ofd;

  int result;

  for (;;)
    {
      ssize_t size = (len > BLOCKSIZE ? BLOCKSIZE : len);
      if (send (ofd, buffer, size, MSG_MORE) != size)
        {
          result = -EAFNOSUPPORT;
          break;
        }
      buffer += size;
      len -= size;
      if (len == 0)
        {
          result = read (ofd, resblock, hashlen) == hashlen ? 0 : -EAFNOSUPPORT;
          break;
        }
    }

  close (ofd);
  return result;
}

int
afalg_stream (FILE *stream, const char *alg,
              void *resblock, ssize_t hashlen)
{
  int ofd = alg_socket (alg);
  if (ofd < 0)
    return ofd;

  /* If STREAM's size is known and nonzero and not too large, attempt
     sendfile to pipe the data.  The nonzero restriction avoids issues
     with /proc files that pretend to be empty, and lets the classic
     read-write loop work around an empty-input bug noted below.  */
  int fd = fileno (stream);
  int result;
  struct stat st;
  off_t off = ftello (stream);
  if (0 <= off && fstat (fd, &st) == 0
      && (S_ISREG (st.st_mode) || S_TYPEISSHM (&st) || S_TYPEISTMO (&st))
      && off < st.st_size && st.st_size - off < SYS_BUFSIZE_MAX)
    {
      /* Make sure the offset of fileno (stream) reflects how many bytes
         have been read from stream before this function got invoked.
         Note: fflush on an input stream after ungetc does not work as expected
         on some platforms.  Therefore this situation is not supported here.  */
      if (fflush (stream))
        result = -EIO;
      else
        {
          off_t nbytes = st.st_size - off;
          if (sendfile (ofd, fd, &off, nbytes) == nbytes)
            {
              if (read (ofd, resblock, hashlen) == hashlen)
                {
                  /* The input buffers of stream are no longer valid.  */
                  if (lseek (fd, off, SEEK_SET) != (off_t)-1)
                    result = 0;
                  else
                    /* The file position of fd has not changed.  */
                    result = -EAFNOSUPPORT;
                }
              else
                /* The file position of fd has not changed.  */
                result = -EAFNOSUPPORT;
            }
          else
            /* The file position of fd has not changed.  */
            result = -EAFNOSUPPORT;
       }
    }
  else
    {
      /* sendfile not possible, do a classic read-write loop.  */

      /* Number of bytes to seek (backwards) in case of error.  */
      off_t nseek = 0;

      for (;;)
        {
          char buf[BLOCKSIZE];
          /* When the stream is not seekable, start with a single-byte block,
             so that we can use ungetc() in the case that send() fails.  */
          size_t blocksize = (nseek == 0 && off < 0 ? 1 : BLOCKSIZE);
          ssize_t size = fread (buf, 1, blocksize, stream);
          if (size == 0)
            {
              /* On Linux < 4.9, the value for an empty stream is wrong (all 0).
                 See <https://patchwork.kernel.org/patch/9308641/>.
                 This was not fixed properly until November 2016,
                 see <https://patchwork.kernel.org/patch/9434741/>.  */
              result = ferror (stream) ? -EIO : nseek == 0 ? -EAFNOSUPPORT : 0;
              break;
            }
          nseek -= size;
          if (send (ofd, buf, size, MSG_MORE) != size)
            {
              if (nseek == -1)
                {
                  /* 1 byte of pushback buffer is guaranteed on stream, even
                     if stream is not seekable.  */
                  ungetc ((unsigned char) buf[0], stream);
                  result = -EAFNOSUPPORT;
                }
              else if (fseeko (stream, nseek, SEEK_CUR) == 0)
                /* The position of stream has been restored.  */
                result = -EAFNOSUPPORT;
              else
                result = -EIO;
              break;
            }

          /* Don't assume that EOF is sticky. See:
             <https://sourceware.org/bugzilla/show_bug.cgi?id=19476>.  */
          if (feof (stream))
            {
              result = 0;
              break;
            }
        }

      if (result == 0 && read (ofd, resblock, hashlen) != hashlen)
        {
          if (nseek == 0 || fseeko (stream, nseek, SEEK_CUR) == 0)
            /* The position of stream has been restored.  */
            result = -EAFNOSUPPORT;
          else
            result = -EIO;
        }
    }
  close (ofd);
  return result;
}

#endif
