pspp-convert: Support decrypting encrypted SPSS syntax files.
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 26 Sep 2015 23:42:13 +0000 (16:42 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 26 Sep 2015 23:42:13 +0000 (16:42 -0700)
Thanks to charlesjohnsont@outlook.com for providing an example.
Bug #45974.

13 files changed:
NEWS
doc/pspp-convert.texi
src/data/automake.mk
src/data/encrypted-file.c [new file with mode: 0644]
src/data/encrypted-file.h [new file with mode: 0644]
src/data/sys-file-encryption.c [deleted file]
src/data/sys-file-encryption.h [deleted file]
tests/automake.mk
tests/data/encrypted-file.at [new file with mode: 0644]
tests/data/sys-file-encryption.at [deleted file]
tests/data/test-encrypted.sps [new file with mode: 0644]
utilities/pspp-convert.1
utilities/pspp-convert.c

diff --git a/NEWS b/NEWS
index 5da8bf74ddff1efe1c034df2e5932162cda3fefd..b1031e7edbdc388ced578f709228dc1e47b927f5 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,11 @@ Changes since 0.8.5:
  * The graphical user interface uses Gtk+ version 3 instead of version 2.
    Accordingly, it has a somewhat different look and feel.
 
+ * The pspp-convert utility can now decrypt encrypted syntax files.
+   The encrypted syntax file format is unacceptably insecure, so to
+   discourage its use PSPP and PSPPIRE do not directly read or write
+   this format.
+
  * Bug fixes, including the following notable ones:
 
    - The correlation coefficient in the paired samples t-test
index 838f81167cf6b6be1850d4f1d6daf278ca90b16d..3719de224c44dcb5fb1af537a417969e33c491a5 100644 (file)
@@ -4,7 +4,8 @@
 @cindex @command{pspp-convert}
 
 @command{pspp-convert} is a command-line utility accompanying
-@pspp{}. It reads an SPSS system or portable file @var{input} and
+@pspp{}. It reads an SPSS or SPSS/PC+ system file or SPSS portable
+file or encrypted SPSS syntax file @var{input} and
 writes a copy of it to another @var{output} in a different format.
 Synopsis:
 
@@ -38,13 +39,19 @@ SPSS system file.
 
 @item por
 SPSS portable file.
+
+@item sps
+SPSS syntax file.  (Only encrypted syntax files may be converted to
+this format.)
 @end table
 
-As a special case of format conversion, @command{pspp-convert} can
-decrypt an encrypted SPSS system file.  Specify the encrypted file as
-@var{input}.  The output will be the equivalent plaintext SPSS system
-file.  You will be prompted for the password (or use @option{-p},
-documented below).
+@command{pspp-convert} can convert most input formats to most output
+formats.  Encrypted system file and syntax files are exceptions: if
+the input file is in an encrypted format, then the output file must be
+the same format (decrypted).  To decrypt such a file, specify the
+encrypted file as @var{input}.  The output will be the equivalent
+plaintext file.  You will be prompted for the password (or use
+@option{-p}, documented below).
 
 Use @code{-O @var{extension}} to override the inferred format or to
 specify the format for unrecognized extensions.
@@ -72,9 +79,10 @@ and SPSS/PC+ system files, do not self-identify their encoding.
 
 @item -p @var{password}
 @item --password=@var{password}
-Specifies the password to use to decrypt an encrypted SPSS system
-file.  If this option is not specified, @command{pspp-convert} will
-prompt interactively for the password as necessary.
+Specifies the password to use to decrypt an encrypted SPSS system file
+or syntax file.  If this option is not specified,
+@command{pspp-convert} will prompt interactively for the password as
+necessary.
 
 Be aware that command-line options, including passwords, may be
 visible to other users on multiuser systems.
index 8b26f525e45576d2264f2a5a731c445fa1adaa68..8031de188a707523142fa90af350a94cb8078f16 100644 (file)
@@ -58,6 +58,8 @@ src_data_libdata_la_SOURCES = \
        src/data/dict-class.h \
        src/data/dictionary.c \
        src/data/dictionary.h \
+       src/data/encrypted-file.c \
+       src/data/encrypted-file.h \
        src/data/file-handle-def.c \
        src/data/file-handle-def.h \
        src/data/file-name.c \
@@ -99,8 +101,6 @@ src_data_libdata_la_SOURCES = \
        src/data/subcase.c \
        src/data/subcase.h \
        src/data/sys-file-encoding.c \
-       src/data/sys-file-encryption.c \
-       src/data/sys-file-encryption.h \
        src/data/sys-file-private.c \
        src/data/sys-file-private.h \
        src/data/sys-file-reader.c \
diff --git a/src/data/encrypted-file.c b/src/data/encrypted-file.c
new file mode 100644 (file)
index 0000000..b90126e
--- /dev/null
@@ -0,0 +1,377 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2013, 2015 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 of the License, 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include "data/encrypted-file.h"
+
+#include <errno.h>
+#include <stdlib.h>
+
+#include "data/file-name.h"
+#include "libpspp/assertion.h"
+#include "libpspp/cast.h"
+#include "libpspp/cmac-aes256.h"
+#include "libpspp/message.h"
+
+#include "gl/minmax.h"
+#include "gl/rijndael-alg-fst.h"
+#include "gl/xalloc.h"
+
+#include "gettext.h"
+#define _(msgid) gettext (msgid)
+
+struct encrypted_file
+  {
+    FILE *file;
+    enum { SYSTEM, SYNTAX } type;
+    int error;
+
+    uint8_t ciphertext[16];
+    uint8_t plaintext[16];
+    unsigned int ofs, n;
+
+    uint32_t rk[4 * (RIJNDAEL_MAXNR + 1)];
+    int Nr;
+  };
+
+static bool try_password(struct encrypted_file *, const char *password);
+static bool decode_password (const char *input, char output[11]);
+static bool fill_buffer (struct encrypted_file *);
+
+/* If FILENAME names an encrypted SPSS file, returns 1 and initializes *FP
+   for further use by the caller.
+
+   If FILENAME can be opened and read, but is not an encrypted SPSS file,
+   returns 0.
+
+   If FILENAME cannot be open or read, returns a negative errno value. */
+int
+encrypted_file_open (struct encrypted_file **fp, const char *filename)
+{
+  struct encrypted_file *f;
+  char header[36 + 16];
+  int retval;
+  int n;
+
+  f = xmalloc (sizeof *f);
+  f->error = 0;
+  f->file = fn_open (filename, "rb");
+  if (f->file == NULL)
+    {
+      msg (ME, _("An error occurred while opening `%s': %s."),
+           filename, strerror (errno));
+      retval = -errno;
+      goto error;
+    }
+
+  n = fread (header, 1, sizeof header, f->file);
+  if (n != sizeof header)
+    {
+      int error = feof (f->file) ? 0 : errno;
+      if (error)
+        msg (ME, _("An error occurred while reading `%s': %s."),
+             filename, strerror (error));
+      retval = -error;
+      goto error;
+    }
+
+  if (!memcmp (header + 8, "ENCRYPTEDSAV", 12))
+    f->type = SYSTEM;
+  else if (!memcmp (header + 8, "ENCRYPTEDSPS", 12))
+    f->type = SYNTAX;
+  else
+    {
+      retval = 0;
+      goto error;
+    }
+
+  memcpy (f->ciphertext, header + 36, 16);
+  f->n = 16;
+  f->ofs = 0;
+  *fp = f;
+  return 1;
+
+error:
+  if (f->file)
+    fn_close (filename, f->file);
+  free (f);
+  *fp = NULL;
+
+  return retval;
+}
+
+/* Attempts to use PASSWORD, which may be a plaintext or "encrypted" password,
+   to unlock F.  Returns true if successful, otherwise false. */
+bool
+encrypted_file_unlock (struct encrypted_file *f, const char *password)
+{
+  char decoded_password[11];
+
+  return (try_password (f, password)
+          || (decode_password (password, decoded_password)
+              && try_password (f, decoded_password)));
+}
+
+/* Attempts to read N bytes of plaintext from F into BUF.  Returns the number
+   of bytes successfully read.  A return value less than N may indicate end of
+   file or an error; use encrypted_file_close() to distinguish.
+
+   This function can only be used after encrypted_file_unlock() returns
+   true. */
+size_t
+encrypted_file_read (struct encrypted_file *f, void *buf_, size_t n)
+{
+  uint8_t *buf = buf_;
+  size_t ofs = 0;
+
+  if (f->error)
+    return 0;
+
+  while (ofs < n)
+    {
+      unsigned int chunk = MIN (n - ofs, f->n - f->ofs);
+      if (chunk > 0)
+        {
+          memcpy (buf + ofs, &f->plaintext[f->ofs], chunk);
+          ofs += chunk;
+          f->ofs += chunk;
+        }
+      else
+        {
+          if (!fill_buffer (f))
+            return ofs;
+        }
+    }
+
+  return ofs;
+}
+
+/* Closes F.  Returns 0 if no read errors occurred, otherwise a positive errno
+   value. */
+int
+encrypted_file_close (struct encrypted_file *f)
+{
+  int error = f->error;
+  if (fclose (f->file) == EOF && !error)
+    error = errno;
+  free (f);
+
+  return error;
+}
+
+/* Returns true if F is an encrypted system file,
+   false if it is an encrypted syntax file. */
+bool
+encrypted_file_is_sav (const struct encrypted_file *f)
+{
+  return f->type == SYSTEM;
+}
+\f
+#define b(x) (1 << (x))
+
+static const uint16_t m0[4][2] = {
+  { b(2),                         b(2) | b(3) | b(6) | b(7) },
+  { b(3),                         b(0) | b(1) | b(4) | b(5) },
+  { b(4) | b(7),                  b(8) | b(9) | b(12) | b(14) },
+  { b(5) | b(6),                  b(10) | b(11) | b(14) | b(15) },
+};
+
+static const uint16_t m1[4][2] = {
+  { b(0) | b(3) | b(12) | b(15),  b(0) | b(1) | b(4) | b(5) },
+  { b(1) | b(2) | b(13) | b(14),  b(2) | b(3) | b(6) | b(7) },
+  { b(4) | b(7) | b(8) | b(11),   b(8) | b(9) | b(12) | b(13) },
+  { b(5) | b(6) | b(9) | b(10),   b(10) | b(11) | b(14) | b(15) },
+};
+
+static const uint16_t m2[4][2] = {
+  { b(2),                         b(1) | b(3) | b(9) | b(11) },
+  { b(3),                         b(0) | b(2) | b(8) | b(10) },
+  { b(4) | b(7),                  b(4) | b(6) | b(12) | b(14) },
+  { b(5) | b(6),                  b(5) | b(7) | b(13) | b(15) },
+};
+
+static const uint16_t m3[4][2] = {
+  { b(0) | b(3) | b(12) | b(15),  b(0) | b(2) | b(8) | b(10) },
+  { b(1) | b(2) | b(13) | b(14),  b(1) | b(3) | b(9) | b(11) },
+  { b(4) | b(7) | b(8) | b(11),   b(4) | b(6) | b(12) | b(14) },
+  { b(5) | b(6) | b(9) | b(10),   b(5) | b(7) | b(13) | b(15) },
+};
+
+static int
+decode_nibble (const uint16_t table[4][2], int nibble)
+{
+  int i;
+
+  for (i = 0; i < 4; i++)
+    if (table[i][0] & (1 << nibble))
+      return table[i][1];
+
+  return 0;
+}
+
+/* Returns true if X has exactly one 1-bit, false otherwise. */
+static bool
+is_pow2 (int x)
+{
+  return x && (x & (x - 1)) == 0;
+}
+
+/* If X has exactly one 1-bit, returns its index, where bit 0 is the LSB.
+   Otherwise, returns 0. */
+static int
+find_1bit (uint16_t x)
+{
+  int i;
+
+  if (!is_pow2 (x))
+    return -1;
+
+  for (i = 0; i < 16; i++)
+    if (x & (1u << i))
+      return i;
+
+  abort ();
+}
+
+/* Attempts to decode a pair of encoded password characters A and B into a
+   single byte of the plaintext password.  Returns 0 if A and B are not a valid
+   encoded password pair, otherwise a byte of the plaintext password. */
+static int
+decode_password_2bytes (uint8_t a, uint8_t b)
+{
+  int x = find_1bit (decode_nibble (m0, a >> 4) & decode_nibble (m2, b >> 4));
+  int y = find_1bit (decode_nibble (m1, a & 15) & decode_nibble (m3, b & 15));
+  return x < 0 || y < 0 ? 0 : (x << 4) | y;
+}
+
+/* Decodes an SPSS so-called "encrypted" password INPUT into OUTPUT.
+
+   An encoded password is always an even number of bytes long and no longer
+   than 20 bytes.  A decoded password is never longer than 10 bytes plus a null
+   terminator.
+
+   Returns true if successful, otherwise false. */
+static bool
+decode_password (const char *input, char output[11])
+{
+  size_t len;
+
+  len = strlen (input);
+  if (len > 20 || len % 2)
+    return false;
+
+  for (; *input; input += 2)
+    {
+      int c = decode_password_2bytes (input[0], input[1]);
+      if (!c)
+        return false;
+      *output++ = c;
+    }
+  *output = '\0';
+
+  return true;
+}
+
+/* If CIPHERTEXT is the first ciphertext block in an encrypted .sav file for
+   PASSWORD, initializes rk[] and returns an nonzero Nr value.
+
+   Otherwise, returns zero. */
+static bool
+try_password(struct encrypted_file *f, const char *password)
+{
+  /* NIST SP 800-108 fixed data. */
+  static const uint8_t fixed[] = {
+    /* i */
+    0x00, 0x00, 0x00, 0x01,
+
+    /* label */
+    0x35, 0x27, 0x13, 0xcc, 0x53, 0xa7, 0x78, 0x89,
+    0x87, 0x53, 0x22, 0x11, 0xd6, 0x5b, 0x31, 0x58,
+    0xdc, 0xfe, 0x2e, 0x7e, 0x94, 0xda, 0x2f, 0x00,
+    0xcc, 0x15, 0x71, 0x80, 0x0a, 0x6c, 0x63, 0x53,
+
+    /* delimiter */
+    0x00,
+
+    /* context */
+    0x38, 0xc3, 0x38, 0xac, 0x22, 0xf3, 0x63, 0x62,
+    0x0e, 0xce, 0x85, 0x3f, 0xb8, 0x07, 0x4c, 0x4e,
+    0x2b, 0x77, 0xc7, 0x21, 0xf5, 0x1a, 0x80, 0x1d,
+    0x67, 0xfb, 0xe1, 0xe1, 0x83, 0x07, 0xd8, 0x0d,
+
+    /* L */
+    0x00, 0x00, 0x01, 0x00,
+  };
+
+  char padded_password[32];
+  size_t password_len;
+  uint8_t cmac[16];
+  uint8_t key[32];
+
+  /* Truncate password to at most 10 bytes. */
+  password_len = strlen (password);
+  if (password_len > 10)
+    password_len = 10;
+
+  /* padded_password = password padded with zeros to 32 bytes. */
+  memset (padded_password, 0, sizeof padded_password);
+  memcpy (padded_password, password, password_len);
+
+  /* cmac = CMAC(padded_password, fixed). */
+  cmac_aes256 (CHAR_CAST (const uint8_t *, padded_password),
+               fixed, sizeof fixed, cmac);
+
+  /* The key is the cmac repeated twice. */
+  memcpy(key, cmac, 16);
+  memcpy(key + 16, cmac, 16);
+
+  /* Use key to initialize AES. */
+  assert (sizeof key == 32);
+  f->Nr = rijndaelKeySetupDec (f->rk, CHAR_CAST (const char *, key), 256);
+
+  /* Check for magic number at beginning of plaintext. */
+  rijndaelDecrypt (f->rk, f->Nr,
+                   CHAR_CAST (const char *, f->ciphertext),
+                   CHAR_CAST (char *, f->plaintext));
+  return !memcmp (f->plaintext, f->type == SYSTEM ? "$FL" : "* E", 3);
+}
+
+static bool
+fill_buffer (struct encrypted_file *f)
+{
+  f->n = fread (f->ciphertext, 1, sizeof f->ciphertext, f->file);
+  f->ofs = 0;
+  if (f->n == sizeof f->ciphertext)
+    {
+      rijndaelDecrypt (f->rk, f->Nr,
+                       CHAR_CAST (const char *, f->ciphertext),
+                       CHAR_CAST (char *, f->plaintext));
+      if (f->type == SYNTAX)
+        {
+          const char *eof = memchr (f->plaintext, '\04', sizeof f->plaintext);
+          if (eof)
+            f->n = CHAR_CAST (const uint8_t *, eof) - f->plaintext;
+        }
+      return true;
+    }
+  else
+    {
+      if (ferror (f->file))
+        f->error = errno;
+      return false;
+    }
+}
diff --git a/src/data/encrypted-file.h b/src/data/encrypted-file.h
new file mode 100644 (file)
index 0000000..d089a04
--- /dev/null
@@ -0,0 +1,34 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2013, 2015 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 of the License, 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 <http://www.gnu.org/licenses/>. */
+
+#ifndef ENCRYPTED_FILE_H
+#define ENCRYPTED_FILE_H 1
+
+#include <stdbool.h>
+#include <stdio.h>
+
+/* Reading encrypted SPSS files. */
+
+struct encrypted_file;
+
+int encrypted_file_open (struct encrypted_file **, const char *filename);
+bool encrypted_file_unlock (struct encrypted_file *, const char *password);
+size_t encrypted_file_read (struct encrypted_file *, void *, size_t);
+int encrypted_file_close (struct encrypted_file *);
+
+bool encrypted_file_is_sav (const struct encrypted_file *);
+
+#endif /* encrypted-file.h */
diff --git a/src/data/sys-file-encryption.c b/src/data/sys-file-encryption.c
deleted file mode 100644 (file)
index 51e253f..0000000
+++ /dev/null
@@ -1,356 +0,0 @@
-/* PSPP - a program for statistical analysis.
-   Copyright (C) 2013 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 of the License, 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 <http://www.gnu.org/licenses/>. */
-
-#include <config.h>
-
-#include "data/sys-file-encryption.h"
-
-#include <errno.h>
-#include <stdlib.h>
-
-#include "data/file-name.h"
-#include "libpspp/assertion.h"
-#include "libpspp/cast.h"
-#include "libpspp/cmac-aes256.h"
-#include "libpspp/message.h"
-
-#include "gl/minmax.h"
-#include "gl/rijndael-alg-fst.h"
-#include "gl/xalloc.h"
-
-#include "gettext.h"
-#define _(msgid) gettext (msgid)
-
-struct encrypted_sys_file
-  {
-    FILE *file;
-    int error;
-
-    uint8_t ciphertext[16];
-    uint8_t plaintext[16];
-    unsigned int n;
-
-    uint32_t rk[4 * (RIJNDAEL_MAXNR + 1)];
-    int Nr;
-  };
-
-static bool try_password(struct encrypted_sys_file *, const char *password);
-static bool decode_password (const char *input, char output[11]);
-static bool fill_buffer (struct encrypted_sys_file *);
-
-/* If FILENAME names an encrypted system file, returns 1 and initializes *FP
-   for further use by the caller.
-
-   If FILENAME can be opened and read, but is not an encrypted system file,
-   returns 0.
-
-   If FILENAME cannot be open or read, returns a negative errno value. */
-int
-encrypted_sys_file_open (struct encrypted_sys_file **fp, const char *filename)
-{
-  struct encrypted_sys_file *f;
-  char header[36 + 16];
-  int retval;
-  int n;
-
-  f = xmalloc (sizeof *f);
-  f->error = 0;
-  f->file = fn_open (filename, "rb");
-  if (f->file == NULL)
-    {
-      msg (ME, _("An error occurred while opening `%s': %s."),
-           filename, strerror (errno));
-      retval = -errno;
-      goto error;
-    }
-
-  n = fread (header, 1, sizeof header, f->file);
-  if (n != sizeof header)
-    {
-      int error = feof (f->file) ? 0 : errno;
-      if (error)
-        msg (ME, _("An error occurred while reading `%s': %s."),
-             filename, strerror (error));
-      retval = -error;
-      goto error;
-    }
-
-  if (memcmp (header + 8, "ENCRYPTEDSAV", 12))
-    {
-      retval = 0;
-      goto error;
-    }
-
-  memcpy (f->ciphertext, header + 36, 16);
-  f->n = 16;
-  *fp = f;
-  return 1;
-
-error:
-  if (f->file)
-    fn_close (filename, f->file);
-  free (f);
-  *fp = NULL;
-
-  return retval;
-}
-
-/* Attempts to use PASSWORD, which may be a plaintext or "encrypted" password,
-   to unlock F.  Returns true if successful, otherwise false. */
-bool
-encrypted_sys_file_unlock (struct encrypted_sys_file *f, const char *password)
-{
-  char decoded_password[11];
-
-  return (try_password (f, password)
-          || (decode_password (password, decoded_password)
-              && try_password (f, decoded_password)));
-}
-
-/* Attempts to read N bytes of plaintext from F into BUF.  Returns the number
-   of bytes successfully read.  A return value less than N may indicate end of
-   file or an error; use encrypted_sys_file_close() to distinguish.
-
-   This function can only be used after encrypted_sys_file_unlock() returns
-   true. */
-size_t
-encrypted_sys_file_read (struct encrypted_sys_file *f, void *buf_, size_t n)
-{
-  uint8_t *buf = buf_;
-  size_t ofs = 0;
-
-  if (f->error)
-    return 0;
-
-  while (ofs < n)
-    {
-      unsigned int chunk = MIN (n - ofs, f->n);
-      if (chunk > 0)
-        {
-          memcpy (buf + ofs, &f->plaintext[16 - f->n], chunk);
-          ofs += chunk;
-          f->n -= chunk;
-        }
-      else
-        {
-          if (!fill_buffer (f))
-            return ofs;
-        }
-    }
-
-  return ofs;
-}
-
-/* Closes F.  Returns 0 if no read errors occurred, otherwise a positive errno
-   value. */
-int
-encrypted_sys_file_close (struct encrypted_sys_file *f)
-{
-  int error = f->error;
-  if (fclose (f->file) == EOF && !error)
-    error = errno;
-  free (f);
-
-  return error;
-}
-\f
-#define b(x) (1 << (x))
-
-static const uint16_t m0[4][2] = {
-  { b(2),                         b(2) | b(3) | b(6) | b(7) },
-  { b(3),                         b(0) | b(1) | b(4) | b(5) },
-  { b(4) | b(7),                  b(8) | b(9) | b(12) | b(14) },
-  { b(5) | b(6),                  b(10) | b(11) | b(14) | b(15) },
-};
-
-static const uint16_t m1[4][2] = {
-  { b(0) | b(3) | b(12) | b(15),  b(0) | b(1) | b(4) | b(5) },
-  { b(1) | b(2) | b(13) | b(14),  b(2) | b(3) | b(6) | b(7) },
-  { b(4) | b(7) | b(8) | b(11),   b(8) | b(9) | b(12) | b(13) },
-  { b(5) | b(6) | b(9) | b(10),   b(10) | b(11) | b(14) | b(15) },
-};
-
-static const uint16_t m2[4][2] = {
-  { b(2),                         b(1) | b(3) | b(9) | b(11) },
-  { b(3),                         b(0) | b(2) | b(8) | b(10) },
-  { b(4) | b(7),                  b(4) | b(6) | b(12) | b(14) },
-  { b(5) | b(6),                  b(5) | b(7) | b(13) | b(15) },
-};
-
-static const uint16_t m3[4][2] = {
-  { b(0) | b(3) | b(12) | b(15),  b(0) | b(2) | b(8) | b(10) },
-  { b(1) | b(2) | b(13) | b(14),  b(1) | b(3) | b(9) | b(11) },
-  { b(4) | b(7) | b(8) | b(11),   b(4) | b(6) | b(12) | b(14) },
-  { b(5) | b(6) | b(9) | b(10),   b(5) | b(7) | b(13) | b(15) },
-};
-
-static int
-decode_nibble (const uint16_t table[4][2], int nibble)
-{
-  int i;
-
-  for (i = 0; i < 4; i++)
-    if (table[i][0] & (1 << nibble))
-      return table[i][1];
-
-  return 0;
-}
-
-/* Returns true if X has exactly one 1-bit, false otherwise. */
-static bool
-is_pow2 (int x)
-{
-  return x && (x & (x - 1)) == 0;
-}
-
-/* If X has exactly one 1-bit, returns its index, where bit 0 is the LSB.
-   Otherwise, returns 0. */
-static int
-find_1bit (uint16_t x)
-{
-  int i;
-
-  if (!is_pow2 (x))
-    return -1;
-
-  for (i = 0; i < 16; i++)
-    if (x & (1u << i))
-      return i;
-
-  abort ();
-}
-
-/* Attempts to decode a pair of encoded password characters A and B into a
-   single byte of the plaintext password.  Returns 0 if A and B are not a valid
-   encoded password pair, otherwise a byte of the plaintext password. */
-static int
-decode_password_2bytes (uint8_t a, uint8_t b)
-{
-  int x = find_1bit (decode_nibble (m0, a >> 4) & decode_nibble (m2, b >> 4));
-  int y = find_1bit (decode_nibble (m1, a & 15) & decode_nibble (m3, b & 15));
-  return x < 0 || y < 0 ? 0 : (x << 4) | y;
-}
-
-/* Decodes an SPSS so-called "encrypted" password INPUT into OUTPUT.
-
-   An encoded password is always an even number of bytes long and no longer
-   than 20 bytes.  A decoded password is never longer than 10 bytes plus a null
-   terminator.
-
-   Returns true if successful, otherwise false. */
-static bool
-decode_password (const char *input, char output[11])
-{
-  size_t len;
-
-  len = strlen (input);
-  if (len > 20 || len % 2)
-    return false;
-
-  for (; *input; input += 2)
-    {
-      int c = decode_password_2bytes (input[0], input[1]);
-      if (!c)
-        return false;
-      *output++ = c;
-    }
-  *output = '\0';
-
-  return true;
-}
-
-/* If CIPHERTEXT is the first ciphertext block in an encrypted .sav file for
-   PASSWORD, initializes rk[] and returns an nonzero Nr value.
-
-   Otherwise, returns zero. */
-static bool
-try_password(struct encrypted_sys_file *f, const char *password)
-{
-  /* NIST SP 800-108 fixed data. */
-  static const uint8_t fixed[] = {
-    /* i */
-    0x00, 0x00, 0x00, 0x01,
-
-    /* label */
-    0x35, 0x27, 0x13, 0xcc, 0x53, 0xa7, 0x78, 0x89,
-    0x87, 0x53, 0x22, 0x11, 0xd6, 0x5b, 0x31, 0x58,
-    0xdc, 0xfe, 0x2e, 0x7e, 0x94, 0xda, 0x2f, 0x00,
-    0xcc, 0x15, 0x71, 0x80, 0x0a, 0x6c, 0x63, 0x53,
-
-    /* delimiter */
-    0x00,
-
-    /* context */
-    0x38, 0xc3, 0x38, 0xac, 0x22, 0xf3, 0x63, 0x62,
-    0x0e, 0xce, 0x85, 0x3f, 0xb8, 0x07, 0x4c, 0x4e,
-    0x2b, 0x77, 0xc7, 0x21, 0xf5, 0x1a, 0x80, 0x1d,
-    0x67, 0xfb, 0xe1, 0xe1, 0x83, 0x07, 0xd8, 0x0d,
-
-    /* L */
-    0x00, 0x00, 0x01, 0x00,
-  };
-
-  char padded_password[32];
-  size_t password_len;
-  uint8_t cmac[16];
-  uint8_t key[32];
-
-  /* Truncate password to at most 10 bytes. */
-  password_len = strlen (password);
-  if (password_len > 10)
-    password_len = 10;
-
-  /* padded_password = password padded with zeros to 32 bytes. */
-  memset (padded_password, 0, sizeof padded_password);
-  memcpy (padded_password, password, password_len);
-
-  /* cmac = CMAC(padded_password, fixed). */
-  cmac_aes256 (CHAR_CAST (const uint8_t *, padded_password),
-               fixed, sizeof fixed, cmac);
-
-  /* The key is the cmac repeated twice. */
-  memcpy(key, cmac, 16);
-  memcpy(key + 16, cmac, 16);
-
-  /* Use key to initialize AES. */
-  assert (sizeof key == 32);
-  f->Nr = rijndaelKeySetupDec (f->rk, CHAR_CAST (const char *, key), 256);
-
-  /* Check for magic number "$FL" always present in SPSS .sav file. */
-  rijndaelDecrypt (f->rk, f->Nr,
-                   CHAR_CAST (const char *, f->ciphertext),
-                   CHAR_CAST (char *, f->plaintext));
-  return !memcmp (f->plaintext, "$FL", 3);
-}
-
-static bool
-fill_buffer (struct encrypted_sys_file *f)
-{
-  f->n = fread (f->ciphertext, 1, sizeof f->ciphertext, f->file);
-  if (f->n == sizeof f->ciphertext)
-    {
-      rijndaelDecrypt (f->rk, f->Nr,
-                       CHAR_CAST (const char *, f->ciphertext),
-                       CHAR_CAST (char *, f->plaintext));
-      return true;
-    }
-  else
-    {
-      if (ferror (f->file))
-        f->error = errno;
-      return false;
-    }
-}
diff --git a/src/data/sys-file-encryption.h b/src/data/sys-file-encryption.h
deleted file mode 100644 (file)
index b406cb2..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/* PSPP - a program for statistical analysis.
-   Copyright (C) 2013 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 of the License, 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 <http://www.gnu.org/licenses/>. */
-
-#ifndef SYS_FILE_ENCRYPTION_H
-#define SYS_FILE_ENCRYPTION_H 1
-
-#include <stdbool.h>
-#include <stdio.h>
-
-/* Reading encrypted system files. */
-
-struct encrypted_sys_file;
-
-int encrypted_sys_file_open (struct encrypted_sys_file **,
-                             const char *filename);
-bool encrypted_sys_file_unlock (struct encrypted_sys_file *,
-                                const char *password);
-size_t encrypted_sys_file_read (struct encrypted_sys_file *, void *, size_t);
-int encrypted_sys_file_close (struct encrypted_sys_file *);
-
-#endif /* sys-file-encryption.h */
index 3b9a5535037f4e1d379e20ac4b0d3284531a2b99..188ffa3792a50d40811f20940c007c83a1ce2313 100644 (file)
@@ -283,7 +283,7 @@ TESTSUITE_AT = \
        tests/data/por-file.at \
        tests/data/sys-file-reader.at \
        tests/data/sys-file.at \
-       tests/data/sys-file-encryption.at \
+       tests/data/encrypted-file.at \
        tests/language/command.at \
        tests/language/control/do-if.at \
        tests/language/control/do-repeat.at \
diff --git a/tests/data/encrypted-file.at b/tests/data/encrypted-file.at
new file mode 100644 (file)
index 0000000..241e4e5
--- /dev/null
@@ -0,0 +1,51 @@
+AT_BANNER([encrypted files])
+
+AT_SETUP([decrypt an encrypted system file])
+AT_KEYWORDS([system file decrypt pspp-convert])
+AT_CHECK([pspp-convert $srcdir/data/hotel-encrypted.sav hotel.sav -p pspp])
+AT_CHECK([pspp-convert hotel.sav hotel.csv])
+AT_CHECK([cat hotel.csv], [0], [dnl
+v1,v2,v3,v4,v5
+4,2,3,4,1
+1,1,3,1,1
+5,2,2,3,4
+3,1,3,1,2
+5,3,1,5,3
+1,2,5,4,2
+3,2,4,3,1
+1,4,5,2,1
+3,2,3,1,2
+2,5,4,2,1
+4,2,2,3,5
+2,1,4,1,1
+1,2,5,5,2
+2,3,3,3,1
+4,1,1,1,3
+1,1,5,1,2
+2,5,5,2,2
+])
+AT_CLEANUP
+
+AT_SETUP([decrypt an encrypted syntax file])
+AT_KEYWORDS([syntax file decrypt pspp-convert])
+AT_CHECK([pspp-convert $srcdir/data/test-encrypted.sps test.sps -p password])
+
+# The sample file is not ideal: lines end in CRLF and its last line
+# lacks a new-line, so "sed" and "echo" make it easier to work with.
+AT_CHECK([sed 's/\r//' test.sps; echo], [0], [dnl
+* Encoding: windows-1252.
+DATA LIST LIST /name (a25) quantity (f8).
+BEGIN DATA.
+widgets 10345
+oojars 2345
+dubreys 98
+thingumies 518
+END DATA.
+ @&t@
+LIST.
+ @&t@
+DESCRIPTIVES /quantity
+ /statistics ALL.
+])
+AT_CLEANUP
+
diff --git a/tests/data/sys-file-encryption.at b/tests/data/sys-file-encryption.at
deleted file mode 100644 (file)
index 337595d..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-AT_BANNER([system file encryption])
-
-AT_SETUP([decrypt an encrypted system file])
-AT_KEYWORDS([system file decrypt pspp-convert])
-AT_CHECK([pspp-convert $srcdir/data/hotel-encrypted.sav hotel.sav -p pspp])
-AT_CHECK([pspp-convert hotel.sav hotel.csv])
-AT_CHECK([cat hotel.csv], [0], [dnl
-v1,v2,v3,v4,v5
-4,2,3,4,1
-1,1,3,1,1
-5,2,2,3,4
-3,1,3,1,2
-5,3,1,5,3
-1,2,5,4,2
-3,2,4,3,1
-1,4,5,2,1
-3,2,3,1,2
-2,5,4,2,1
-4,2,2,3,5
-2,1,4,1,1
-1,2,5,5,2
-2,3,3,3,1
-4,1,1,1,3
-1,1,5,1,2
-2,5,5,2,2
-])
-AT_CLEANUP
diff --git a/tests/data/test-encrypted.sps b/tests/data/test-encrypted.sps
new file mode 100644 (file)
index 0000000..58ed181
Binary files /dev/null and b/tests/data/test-encrypted.sps differ
index 2dc608980e2fc6e241b3632be71e64e3ee1206b2..bbbdd62a4c09f1b1906b2aa68d0b164ac8a1cd42 100644 (file)
@@ -7,7 +7,7 @@
 .TH pspp\-convert 1 "October 2013" "PSPP" "PSPP Manual"
 .
 .SH NAME
-pspp\-convert \- convert SPSS system and portable files to other formats
+pspp\-convert \- convert SPSS files to other formats
 .
 .SH SYNOPSIS
 \fBpspp\-convert\fR [\fIoptions\fR] \fIinput\fR \fIoutput\fR
@@ -18,7 +18,8 @@ pspp\-convert \- convert SPSS system and portable files to other formats
 .
 .SH DESCRIPTION
 The \fBpspp\-convert\fR program reads \fIinput\fR, which may be an
-SPSS system file, an SPSS/PC+ system file, or an SPSS portable file,
+SPSS system file, an SPSS/PC+ system file, an SPSS portable file,
+or an encrypted SPSS syntax file,
 and writes it to \fIoutput\fR, performing format conversion as
 necessary.
 .PP
@@ -44,14 +45,19 @@ SPSS system file.
 .IP \fBpor\fR
 SPSS portable file.
 .
+.IP \fBsps\fR
+SPSS syntax file.  (Only encrypted syntax files may be converted to
+this format.)
+.
 .PP
 Use \fB\-O \fIextension\fR to override the inferred format or to
 specify the format for unrecognized extensions.
 .
 .PP
 \fBpspp\-convert\fR can convert most input formats to most output
-formats, with one exception: if the input file is an encrypted system
-file, then the output file must also be an (unencrypted) system file.
+formats.  Encrypted system file and syntax files are exceptions: if
+the input file is in an encrypted format, then the output file must
+be the same format (decrypted).
 .
 .SH "OPTIONS"
 .
@@ -75,7 +81,8 @@ do not self-identify their encoding.
 .
 .IP "\fB\-p \fIpassword\fR"
 .IQ "\fB\-\-password=\fIpassword\fR"
-Specifies the password to use to decrypt an encrypted SPSS system file
+Specifies the password to use to decrypt encrypted SPSS system file
+or syntax file
 \fIinput\fR.  If this option is not specified, \fBpspp\-convert\fR
 prompts for the password.
 .
index ebd340ec3599e3cf942fa0e015a09d51c4c10e56..264ec7a705f7c89b1426eee7e4e3acb09d9d2962 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2013, 2014 Free Software Foundation, Inc.
+   Copyright (C) 2013, 2014, 2015 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
 #include "data/casereader.h"
 #include "data/casewriter.h"
 #include "data/csv-file-writer.h"
+#include "data/encrypted-file.h"
 #include "data/file-name.h"
 #include "data/por-file-writer.h"
 #include "data/settings.h"
-#include "data/sys-file-encryption.h"
 #include "data/sys-file-writer.h"
 #include "data/file-handle-def.h"
 #include "libpspp/assertion.h"
 
 static void usage (void);
 
-static void decrypt_sav_file (struct encrypted_sys_file *enc,
-                              const char *input_filename,
-                              const char *output_filename,
-                              const char *password);
+static void decrypt_file (struct encrypted_file *enc,
+                          const char *input_filename,
+                          const char *output_filename,
+                          const char *password);
 
 int
 main (int argc, char *argv[])
@@ -62,7 +62,7 @@ main (int argc, char *argv[])
   struct casereader *reader;
   struct file_handle *input_fh;
   const char *encoding = NULL;
-  struct encrypted_sys_file *enc;
+  struct encrypted_file *enc;
 
   const char *output_format = NULL;
   struct file_handle *output_fh;
@@ -145,13 +145,22 @@ main (int argc, char *argv[])
       output_format = dot + 1;
     }
 
-  if (encrypted_sys_file_open (&enc, input_filename) > 0)
+  if (encrypted_file_open (&enc, input_filename) > 0)
     {
-      if (strcmp (output_format, "sav") && strcmp (output_format, "sys"))
-        error (1, 0, _("can only convert encrypted data file to sav or sys "
-                       "format"));
+      if (encrypted_file_is_sav (enc))
+        {
+          if (strcmp (output_format, "sav") && strcmp (output_format, "sys"))
+            error (1, 0, _("can only convert encrypted data file to sav or "
+                           "sys format"));
+        }
+      else
+        {
+          if (strcmp (output_format, "sps"))
+            error (1, 0, _("can only convert encrypted syntax file to sps "
+                           "format"));
+        }
 
-      decrypt_sav_file (enc, input_filename, output_filename, password);
+      decrypt_file (enc, input_filename, output_filename, password);
       goto exit;
     }
 
@@ -214,10 +223,10 @@ exit:
 }
 
 static void
-decrypt_sav_file (struct encrypted_sys_file *enc,
-                  const char *input_filename,
-                  const char *output_filename,
-                  const char *password)
+decrypt_file (struct encrypted_file *enc,
+              const char *input_filename,
+              const char *output_filename,
+              const char *password)
 {
   FILE *out;
   int err;
@@ -229,7 +238,7 @@ decrypt_sav_file (struct encrypted_sys_file *enc,
         exit (1);
     }
 
-  if (!encrypted_sys_file_unlock (enc, password))
+  if (!encrypted_file_unlock (enc, password))
     error (1, 0, _("sorry, wrong password"));
 
   out = fn_open (output_filename, "wb");
@@ -241,7 +250,7 @@ decrypt_sav_file (struct encrypted_sys_file *enc,
       uint8_t buffer[1024];
       size_t n;
 
-      n = encrypted_sys_file_read (enc, buffer, sizeof buffer);
+      n = encrypted_file_read (enc, buffer, sizeof buffer);
       if (n == 0)
         break;
 
@@ -249,7 +258,7 @@ decrypt_sav_file (struct encrypted_sys_file *enc,
         error (1, errno, ("%s: write error"), output_filename);
     }
 
-  err = encrypted_sys_file_close (enc);
+  err = encrypted_file_close (enc);
   if (err)
     error (1, err, ("%s: read error"), input_filename);
 
@@ -264,20 +273,21 @@ usage (void)
   printf ("\
 %s, a utility for converting SPSS data files to other formats.\n\
 Usage: %s [OPTION]... INPUT OUTPUT\n\
-where INPUT is an SPSS system or portable file\n\
+where INPUT is an SPSS data file or encrypted syntax file\n\
   and OUTPUT is the name of the desired output file.\n\
 \n\
 The desired format of OUTPUT is by default inferred from its extension:\n\
   csv txt             comma-separated value\n\
   sav sys             SPSS system file\n\
   por                 SPSS portable file\n\
+  sps                 SPSS syntax file (encrypted syntax input files only)\n\
 \n\
 Options:\n\
   -O, --output-format=FORMAT  set specific output format, where FORMAT\n\
                       is one of the extensions listed above\n\
   -e, --encoding=CHARSET  override encoding of input data file\n\
   -c MAXCASES         limit number of cases to copy (default is all cases)\n\
-  -p PASSWORD         password for encrypted .sav files\n\
+  -p PASSWORD         password for encrypted files\n\
   --help              display this help and exit\n\
   --version           output version information and exit\n",
           program_name, program_name);