← Back to team overview

duplicity-team team mailing list archive

[Merge] lp:~lekensteyn/duplicity/multipass into lp:duplicity

 

Lekensteyn has proposed merging lp:~lekensteyn/duplicity/multipass into lp:duplicity.

Requested reviews:
  Lekensteyn (lekensteyn)
  duplicity-team (duplicity-team)
Related bugs:
  Bug #680425 in Duplicity: "Endless retype passphrase when typo"
  https://bugs.launchpad.net/duplicity/+bug/680425
  Bug #793096 in Duplicity: "Allow to pass different passwords for --sign-key and --encrypt-key"
  https://bugs.launchpad.net/duplicity/+bug/793096

For more details, see:
https://code.launchpad.net/~lekensteyn/duplicity/multipass/+merge/64307

Enables the use of a different passphrase for the GPG signing and encryption key. (Closes #793096)
Allows to specify a different secret keyring for the GPG encryption key.
Updated manual page with the above two changes.
Do not keep asking for a passphrase confirmation, but start over on asking the passphrase to prevent an infinite loop. (Closes #680425)

-- 
https://code.launchpad.net/~lekensteyn/duplicity/multipass/+merge/64307
Your team duplicity-team is requested to review the proposed merge of lp:~lekensteyn/duplicity/multipass into lp:duplicity.
=== modified file 'duplicity-bin'
--- duplicity-bin	2011-06-17 06:21:42 +0000
+++ duplicity-bin	2011-06-17 12:44:38 +0000
@@ -58,7 +58,7 @@
 exit_val = None
 
 
-def get_passphrase(n, action):
+def get_passphrase(n, action, for_signing = False):
     """
     Check to make sure passphrase is indeed needed, then get
     the passphrase from environment, from gpg-agent, or user
@@ -68,13 +68,23 @@
     verification for the time being.
 
     @type  n: int
-    @param n: action to perform
+    @param n: verification level for a passphrase being requested
+    @type  action: string
+    @param action: action to perform
+    @type  for_signing: boolean
+    @param for_signing: true if the passphrase is for a signing key, false if not
     @rtype: string
     @return: passphrase
     """
 
     # First try the environment
     try:
+        if for_signing:
+            return os.environ['SIGN_PASSPHRASE']
+    except KeyError:
+        pass
+
+    try:
         return os.environ['PASSPHRASE']
     except KeyError:
         pass
@@ -90,7 +100,7 @@
     if not globals.encryption or globals.use_agent:
         return ""
 
-    # no passphrase if --list-current
+    # no passphrase needed if --list-current
     elif (action == "list-current"):
         return ""
 
@@ -119,28 +129,42 @@
     # Finally, ask the user for the passphrase
     else:
         log.Info("PASSPHRASE variable not set, asking user.")
+        use_cache = True
         while 1:
-            if n == 2:
-                pass1 = globals.gpg_profile.passphrase
-            else:
-                if globals.gpg_profile.passphrase:
+            # ask the user to enter a new passphrase to avoid an infinite loop
+            # if the user made a typo in the first passphrase
+            if use_cache and n == 2:
+                if for_signing:
+                    pass1 = globals.gpg_profile.signing_passphrase
+                else:
                     pass1 = globals.gpg_profile.passphrase
+            else:
+                if for_signing:
+                    if use_cache and globals.gpg_profile.signing_passphrase:
+                        pass1 = globals.gpg_profile.signing_passphrase
+                    else:
+                        pass1 = getpass.getpass("GnuPG passphrase for signing key: ")
                 else:
-                    pass1 = getpass.getpass("GnuPG passphrase: ")
+                    if use_cache and globals.gpg_profile.passphrase:
+                        pass1 = globals.gpg_profile.passphrase
+                    else:
+                        pass1 = getpass.getpass("GnuPG passphrase: ")
 
             if n == 1:
                 pass2 = pass1
+            elif for_signing:
+                pass2 = getpass.getpass("Retype passphrase for signing key to confirm: ")
             else:
                 pass2 = getpass.getpass("Retype passphrase to confirm: ")
 
             if not pass1 == pass2:
                 print "First and second passphrases do not match!  Please try again."
-                n = 3
+                use_cache = False
                 continue
 
-            if not pass1 and not globals.gpg_profile.recipients:
+            if not pass1 and not globals.gpg_profile.recipients and not for_signing:
                 print "Cannot use empty passphrase with symmetric encryption!  Please try again."
-                n = 3
+                use_cache = False
                 continue
 
             return pass1
@@ -959,6 +983,7 @@
         if not globals.dry_run:
             log.Notice(_("Synchronizing remote metadata to local cache..."))
             if local_missing and (rem_needpass or loc_needpass):
+                # password for the --encrypt-key
                 globals.gpg_profile.passphrase = get_passphrase(1, "sync")
             for fn in local_spurious:
                 remove_local(fn)
@@ -1144,19 +1169,16 @@
         else:
             log.FatalError("Unable to locate pydevd.", log.ErrorCode.user_error)
 
-    # get the passphrase if we need to based on action/options
-    globals.gpg_profile.passphrase = get_passphrase(1, action)
-
     # log some debugging status info
     log_startup_parms(log.INFO)
 
     # check for disk space and available file handles
     check_resources(action)
 
-    # check archive synch with remote, fix if needed
+    # synchronize local and remote, the passphrase is requested if needed
     sync_archive()
 
-    # get current collection status
+    # get current collection status, no passphrase needed
     col_stats = collections.CollectionsStatus(globals.backend,
                                               globals.archive_dir).set_values()
 
@@ -1206,6 +1228,11 @@
 
     os.umask(077)
 
+    # full/inc only needs a passphrase for symmetric keys
+    if not action in ["full", "inc"] or not globals.gpg_profile.recipients:
+        # get the passphrase if we need to based on action/options
+        globals.gpg_profile.passphrase = get_passphrase(1, action)
+
     if action == "restore":
         restore(col_stats)
     elif action == "verify":
@@ -1224,16 +1251,36 @@
         sync_archive(col_stats)
     else:
         assert action == "inc" or action == "full", action
+        # the passphrase for full and inc is used by --sign-key
+        # the sign key can have a different passphrase than the encrypt
+        # key, therefore request a passphrase
+        if globals.gpg_profile.sign_key:
+            globals.gpg_profile.signing_passphrase = get_passphrase(3, action, True)
+
+        # if there are no recipients (no --encrypt-key), it must be a
+        # symmetric key. Therefore, confirm the passphrase
+        if not globals.gpg_profile.recipients:
+            globals.gpg_profile.passphrase = get_passphrase(2, action)
+            # a limitation in the GPG implementation does not allow for
+            # inputting different passphrases, this affects symmetric+sign.
+            # Allow an empty passphrase for the key though to allow a non-empty
+            # symmetric key
+            if (globals.gpg_profile.signing_passphrase and
+                globals.gpg_profile.passphrase != globals.gpg_profile.signing_passphrase):
+                log.FatalError("When using symmetric encryption, the signing passphrase must equal the encryption passphrase.", log.ErrorCode.user_error)
+
         if action == "full":
-            globals.gpg_profile.passphrase = get_passphrase(2, action)
             full_backup(col_stats)
         else:  # attempt incremental
             sig_chain = check_sig_chain(col_stats)
+            # action == "inc" was requested, but no full backup is available
             if not sig_chain:
-                globals.gpg_profile.passphrase = get_passphrase(2, action)
                 full_backup(col_stats)
             else:
                 if not globals.restart:
+                    # only ask for a passphrase if there was a previous backup
+                    if col_stats.all_backup_chains:
+                        globals.gpg_profile.passphrase = get_passphrase(1, action)
                     check_last_manifest(col_stats) # not needed for full backup
                 incremental_backup(sig_chain)
     globals.backend.close()

=== modified file 'duplicity.1'
--- duplicity.1	2011-06-14 18:45:49 +0000
+++ duplicity.1	2011-06-17 12:44:38 +0000
@@ -70,10 +70,6 @@
 duplicity supports deleted files, full Unix permissions, directories,
 symbolic links, fifos, etc., but not hard links.
 
-Duplicity will read the PASSPHRASE environment variable to find the
-passphrase to give to GnuPG.  If this is not set, the user will be
-prompted for the passphrase.
-
 If you are backing up the root directory /, remember to --exclude
 /proc, or else duplicity will probably crash on the weird stuff in
 there.
@@ -299,6 +295,15 @@
 symmetric (traditional) encryption.  Can be specified multiple times.
 
 .TP
+.BI "--encrypt-secret-keyring " filename
+This option can only be used with
+.BR --encrypt-key ,
+and changes the path to the secret keyring for the encrypt key to
+.I filename
+This keyring is not used when creating a backup. If not specified, the
+default secret keyring is used which is usually located at .gnupg/secring.gpg
+
+.TP
 .BI "--exclude " shell_pattern
 Exclude the file or files matched by
 .IR shell_pattern .
@@ -728,7 +733,16 @@
 Supported by most backends which are password capable. More secure than
 setting it in the backend url (which might be readable in the operating
 systems process listing to other users on the same machine).
-
+.TP
+.B PASSPHRASE
+This passphrase is passed to GnuPG. If this is not set, the user will be
+prompted for the passphrase.
+.TP
+.B SIGN_PASSPHRASE
+The passphrase to be used for
+.B --sign-key
+, if SIGN_PASSPHRASE is not set but PASSPHRASE is set, the latter will be used.
+Otherwise, if no passphrase is available, the user will be prompted for it.
 
 .SH URL FORMAT
 Duplicity tries to maintain a standard URL format as much as possible.
@@ -1247,6 +1261,17 @@
 Bad signatures will be treated as empty instead of logging appropriate
 error message.
 
+If symmetric encryption is used and the signing key is passphrase-protected, the
+encryption passphrase must equal the passphrase of the signing key. This
+limitation can be circumvented by using
+.B gpg-agent
+for storing the passphrase of the signing key and the
+.B PASSPHRASE
+environment variable for the encryption key or by enabling asymmetric
+encryption using the 
+.B --encrypt-key
+option.
+
 .SH AUTHOR
 Original Author - Ben Escoto <bescoto@xxxxxxxxxxxx>
 

=== modified file 'duplicity/commandline.py'
--- duplicity/commandline.py	2011-06-17 06:21:42 +0000
+++ duplicity/commandline.py	2011-06-17 12:44:38 +0000
@@ -246,6 +246,9 @@
                       dest="", action="callback",
                       callback=lambda o, s, v, p: globals.gpg_profile.recipients.append(v)) #@UndefinedVariable
 
+    # secret keyring in which the private encrypt key can be found
+    parser.add_option("--encrypt-secret-keyring", type="string", metavar=_("path"))
+
     # TRANSL: Used in usage help to represent a "glob" style pattern for
     # matching one or more files, as described in the documentation.
     # Example:

=== modified file 'duplicity/gpg.py'
--- duplicity/gpg.py	2011-06-17 06:21:42 +0000
+++ duplicity/gpg.py	2011-06-17 12:44:38 +0000
@@ -66,7 +66,9 @@
             assert recipients # can only sign with asym encryption
 
         self.passphrase = passphrase
+        self.signing_passphrase = passphrase
         self.sign_key = sign_key
+        self.encrypt_secring = None
         if recipients is not None:
             assert type(recipients) is types.ListType # must be list, not tuple
             self.recipients = recipients
@@ -111,6 +113,17 @@
         if profile.sign_key:
             gnupg.options.default_key = profile.sign_key
             cmdlist.append("--sign")
+        # encrypt: sign key needs passphrase
+        # decrypt: encrypt key needs passphrase
+        # special case: allow different symmetric pass with empty sign pass
+        if encrypt and profile.sign_key and profile.signing_passphrase:
+            passphrase = profile.signing_passphrase
+        else:
+            passphrase = profile.passphrase
+        # in case the passphrase is not set, pass an empty one to prevent
+        # TypeError: expected a character buffer object on .write()
+        if passphrase is None:
+            passphrase = ""
 
         if encrypt:
             if profile.recipients:
@@ -124,17 +137,20 @@
                            attach_fhs={'stdout': encrypt_path.open("wb"),
                                        'stderr': self.stderr_fp,
                                        'logger': self.logger_fp})
-            p1.handles['passphrase'].write(profile.passphrase)
+            p1.handles['passphrase'].write(passphrase)
             p1.handles['passphrase'].close()
             self.gpg_input = p1.handles['stdin']
         else:
+            if profile.recipients and profile.encrypt_secring:
+                cmdlist.append('--secret-keyring')
+                cmdlist.append(profile.encrypt_secring)
             self.status_fp = tempfile.TemporaryFile()
             p1 = gnupg.run(['--decrypt'], create_fhs=['stdout', 'passphrase'],
                            attach_fhs={'stdin': encrypt_path.open("rb"),
                                        'status': self.status_fp,
                                        'stderr': self.stderr_fp,
                                        'logger': self.logger_fp})
-            p1.handles['passphrase'].write(profile.passphrase)
+            p1.handles['passphrase'].write(passphrase)
             p1.handles['passphrase'].close()
             self.gpg_output = p1.handles['stdout']
         self.gpg_process = p1


Follow ups