__all__ = [ "hdr_make", "hdr_read", "hdr_fmt", "hdr_fmt_pretty"
-          , "PDTCRYPT_HDR_SIZE" , "AES_GCM_IV_CNT_DATA", "AES_GCM_IV_CNT_INFOFILE"
+          , "PDTCRYPT_HDR_SIZE", "AES_GCM_IV_CNT_DATA"
+          , "AES_GCM_IV_CNT_INFOFILE", "AES_GCM_IV_CNT_INDEX"
           ]
 
 
 AES_GCM_MAX_SIZE = (1 << 36) - (1 << 5) # 2^39 - 2^8 b ≅ 64 GB
 AES_GCM_FMT_TAG  = "<16s"
 
+# index and info files are written on-the fly while encrypting so their
+# counters must be available inadvance
 AES_GCM_IV_CNT_INFOFILE = 1 # constant
-AES_GCM_IV_CNT_DATA     = AES_GCM_IV_CNT_INFOFILE + 1 # also for multivolume
+AES_GCM_IV_CNT_INDEX    = AES_GCM_IV_CNT_INFOFILE + 1
+AES_GCM_IV_CNT_DATA     = AES_GCM_IV_CNT_INDEX    + 1 # also for multivolume
 AES_GCM_IV_CNT_MAX      = 0xffFFffFF
 
 
          + (len (b) | 1 == len (b) and " %.2x" % b[-1] or "") # odd lengths
 
 
+def hdr_iv_counter (h):
+    """Extract the variable part of the IV of the given header."""
+    _fixed, cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
+    return cnt
+
+
+def hdr_iv_fixed (h):
+    """Extract the fixed part of the IV of the given header."""
+    fixed, _cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
+    return fixed
+
+
 hdr_dump = hex_spaced_of_bytes
 
 
     return partial (fn, params)
 
 
-STATE_FRESH = 0
-STATE_DEAD  = 1
-STATE_LIVE  = 2
-
 class Crypto (object):
     """
     Encryption context to remain alive throughout an entire tarfile pass.
     cnt  = None # file counter (uint32_t != 0)
     iv   = None # current IV
     fixed        = None # accu for 64 bit fixed parts of IV
+    used_ivs     = None # tracks IVs during decryption
     password     = None
     paramversion = None
     stats = { "in"  : 0
 
     ctsize  = -1
     ptsize  = -1
-    info_counter_used = False
+    info_counter_used  = False
+    index_counter_used = False
 
     def __init__ (self, *al, **akv):
         self.set_parameters (*al, **akv)
                 raise InvalidParameter ("attempted to reuse info file counter "
                                         "%d: must be unique" % cnt)
             self.info_counter_used = True
-            return
+        elif cnt == AES_GCM_IV_CNT_INDEX:
+            if self.index_counter_used is True:
+                raise InvalidParameter ("attempted to reuse index file counter "
+                                        "%d: must be unique" % cnt)
+            self.index_counter_used = True
         if cnt <= AES_GCM_IV_CNT_MAX:
             self.cnt = cnt
             return
         if isinstance (filename, str) is False:
             raise InvalidParameter ("next: filename must be a string, no %s"
                                     % type (filename))
-        if counter is not None \
-                and isinstance (counter, int) is False:
-            raise InvalidParameter ("next: the supplied counter is of invalid "
-                                    "type %s; please pass an integer instead"
-                                    % type (counter))
+        if counter is not None:
+            if isinstance (counter, int) is False:
+                raise InvalidParameter ("next: the supplied counter is of "
+                                        "invalid type %s; please pass an "
+                                        "integer instead" % type (counter))
+            self.set_object_counter (counter)
         self.iv = self.iv_make ()
         if self.paramenc == "aes-gcm":
             self.enc = Cipher \
         self.lastinfo = (filename, hdrdum)
         super().next (self.password, self.paramversion, self.nacl)
 
-        self.set_object_counter (counter if counter is not None else self.cnt + 1)
+        self.set_object_counter (self.cnt + 1)
         return hdrdum
 
 
 
 class Decrypt (Crypto):
 
-    tag      = None # GCM tag, part of header
-    used_ivs = None # if a set, panic on duplicate object IV
-    last_iv  = None # check consecutive ivs in strict mode
+    tag        = None   # GCM tag, part of header
+    strict_ivs = False  # if True, panic on duplicate object IV
+    last_iv    = None   # check consecutive ivs in strict mode
 
     def __init__ (self, password, counter=None, fixedparts=None,
                   strict_ivs=False):
             self.fixed.sort ()
             super().__init__ (password, counter=counter)
 
-        if strict_ivs is True:
-            self.used_ivs = set ()
+        self.used_ivs   = set ()
+        self.strict_ivs = strict_ivs
 
         super().__init__ (password, counter=counter)
 
 
 
     def check_duplicate_iv (self, iv):
-        if iv in self.used_ivs:
+        if self.strict_ivs is True and iv in self.used_ivs:
             raise DuplicateIV ("iv [%r] was reused" % iv)
         # vi has not been used before; add to collection
         self.used_ivs.add (iv)
 
     def check_consecutive_iv (self, iv):
         fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
-        if self.last_iv is not None \
+        if self.strict_ivs is True \
+                and self.last_iv is not None \
                 and self.last_iv [0] == fixed \
                 and self.last_iv [1] != cnt - 1:
             raise NonConsecutiveIV ("iv [%r] counter not successor of "
             fixed, _ = struct.unpack (FMT_I2N_IV, iv)
             raise InvalidIVFixedPart ("iv [%r] has invalid fixed part [%r]"
                                       % (iv, fixed))
-        if self.used_ivs is not None:
-            self.check_duplicate_iv   (iv)
-            self.check_consecutive_iv (iv)
+        self.check_duplicate_iv   (iv)
+        self.check_consecutive_iv (iv)
 
         self.tag = tag
         defs = ENCRYPTION_PARAMETERS.get (paramversion, None)
 
         enccounter = None
         if mode == "w":
             crypto_ctx = self.initialize_encryption (CRYPTO_MODE_ENCRYPT)
-            if crypto_ctx is not None:
-                if kind == AUXILIARY_FILE_INFO:
-                    enccounter = crypto.AES_GCM_IV_CNT_INFOFILE
-                elif kind == AUXILIARY_FILE_INDEX:
-                    enccounter = crypto.AES_GCM_IV_CNT_INDEX
-                else:
-                    raise Exception ("invalid kind of aux file %r" % kind)
         elif mode == "r":
             crypto_ctx = self.initialize_encryption (CRYPTO_MODE_DECRYPT)
 
+        if crypto_ctx is not None:
+            if kind == AUXILIARY_FILE_INFO:
+                enccounter = crypto.AES_GCM_IV_CNT_INFOFILE
+            elif kind == AUXILIARY_FILE_INDEX:
+                enccounter = crypto.AES_GCM_IV_CNT_INDEX
+            else:
+                raise Exception ("invalid kind of aux file %r" % kind)
+
         sink = tarfile._Stream(name=path, mode=mode, comptype=comptype,
                                bufsize=tarfile.RECORDSIZE, fileobj=None,
                                encryption=crypto_ctx, enccounter=enccounter)
                                          volume_number=0)
         tarfile_path = os.path.join(backup_path, vol_name)
 
-        # postpone creation of index file to accomodate encryption
-        index_accu = io.BytesIO ()
-
         # init index
         cwd = os.getcwd()
 
+        index_name = self.index_name_func(is_full=False)
+        index_path = os.path.join(backup_path, index_name)
+        index_sink = self.open_auxiliary_file(index_path, 'w')
+
         def new_volume_handler(deltarobj, cwd, backup_path, tarobj, base_name, volume_number):
             '''
             Handles the new volumes
         # wraps some args from context into the handler
         new_volume_handler = partial(new_volume_handler, self, cwd, backup_path)
 
-        index_accu.write(bytes('{"type": "python-delta-tar-index", "version": 1, "backup-type": "diff", "extra_data": %s}\n' % extra_data_str, 'UTF-8'))
+        index_sink.write(bytes('{"type": "python-delta-tar-index", "version": 1, "backup-type": "diff", "extra_data": %s}\n' % extra_data_str, 'UTF-8'))
 
         s = bytes('{"type": "BEGIN-FILE-LIST"}\n', 'UTF-8')
         # calculate checksum and write into the stream
         crc = binascii.crc32(s) & 0xFFFFffff
-        index_accu.write(s)
+        index_sink.write(s)
 
         # start creating the tarfile
         tarobj = tarfile.TarFile.open(tarfile_path,
                 # store the stat dict in the index
                 s = bytes(json.dumps(stat) + '\n', 'UTF-8')
                 crc = binascii.crc32(s, crc) & 0xffffffff
-                index_accu.write(s)
+                index_sink.write(s)
 
         s = bytes('{"type": "END-FILE-LIST"}\n', 'UTF-8')
         crc = binascii.crc32(s, crc) & 0xffffffff
-        index_accu.write(s)
+        index_sink.write(s)
         s = bytes('{"type": "file-list-checksum", "checksum": %d}\n' % crc, 'UTF-8')
-        index_accu.write(s)
+        index_sink.write(s)
 
         index_it.release()
         os.chdir(cwd)
         tarobj.close()
-
-        index_name = self.index_name_func(is_full=False)
-        index_path = os.path.join(backup_path, index_name)
-        index_sink = self.open_auxiliary_file(index_path, 'w')
-        index_sink.write(index_accu.getvalue ())
         index_sink.close()
 
 
         index_it.release()
         os.chdir(cwd)
         helper.cleanup()
+        helper.validate()
 
     def _parse_json_line(self, f, l_no):
         '''
         self._directories = []
         self._deltatar = deltatar
         self._cwd = cwd
-        self.password = deltatar.password
+        self._password = deltatar.password
+        self._decryptors = []
 
         try:
             import grp, pwd
                 is_full = index == index_list[-1]
 
                 decryptor = None
-                if self.password is not None:
-                    decryptor = crypto.Decrypt (self.password)
+                if self._password is not None:
+                    decryptor = crypto.Decrypt (self._password)
 
                 # make paths absolute to avoid cwd problems
                 if not os.path.isabs(index):
             )
             self._data.append(s)
 
+
+    def validate (self):
+        """If encryption was used, verify post-conditions."""
+        if len (self._decryptors) == 0:
+            return
+        acc = None
+        for dec in self._decryptors:
+            if acc is None:
+                acc = dec.used_ivs.copy ()
+            else:
+                used_ivs  = dec.used_ivs
+                intersect = used_ivs & acc
+                if len (intersect) > 0:
+                    raise Exception ("ERROR: %d duplicate IVs found during "
+                                     "decryption" % len (intersect))
+                acc |= used_ivs
+
+
     def cleanup(self):
         '''
         Closes all open files
 
        A stream-like object could be for example: sys.stdin,
        sys.stdout, a socket, a tape device etc.
 
-       _Stream is intended to be used only internally.
+       _Stream is intended to be used only internally but is
+       nevertherless used externally by Deltatar.
+
+       When encrypting, the ``enccounter`` will be used for
+       initializing the first cryptographic context. When
+       decrypting, its value will be compared to the decrypted
+       object. Decryption fails if the value does not match.
+       In effect, this means that a ``_Stream`` whose ctor was
+       passed ``enccounter`` can only be used to encrypt or
+       decrypt a single object.
     """
 
     remainder = -1 # track size in encrypted entries
         if comptype == '':
             comptype = "tar"
 
+        self.enccounter = None
+        if self.arcmode & ARCMODE_ENCRYPT:
+            self.enccounter = enccounter
+
         self.name     = name or ""
         self.mode     = mode
         self.comptype = comptype
                 elif mode == "w":
                     if not (self.arcmode & ARCMODE_CONCAT):
                         if self.arcmode & ARCMODE_ENCRYPT:
-                            self._init_write_encrypt (name, enccounter)
+                            self._init_write_encrypt (name)
                         self._init_write_gz ()
                 self.crc = zlib.crc32(b"") & 0xFFFFffff
 
                 if not (self.arcmode & ARCMODE_CONCAT) \
                         and mode == "w" \
                         and self.arcmode & ARCMODE_ENCRYPT:
-                    self._init_write_encrypt (name, enccounter)
+                    self._init_write_encrypt (name)
 
             else:
                 if self.arcmode & ARCMODE_ENCRYPT:
                                        "processing %r at pos %d"
                                        % (exn, self.fileobj, lasthdr)) \
                       from exn
+            if self.enccounter is not None:
+                # enforce that the iv counter in the header matches an
+                # explicitly requested one
+                iv = crypto.hdr_iv_counter (hdr)
+                if iv != self.enccounter:
+                    raise DecryptionError ("expected IV counter %d, got %d"
+                                           % (self.enccounter, iv))
             self.lasthdr   = lasthdr
             self.remainder = hdr ["ctsize"] # distance to next header
             self.encryption.next (hdr)