← 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:
  duplicity-team (duplicity-team)
Related bugs:
  Bug #684025 in ProjectStats: "Funktionalität von aus den Games in die Tabellen verlagern"
  https://bugs.launchpad.net/projectstats/+bug/684025
  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-04-16 21:25:32 +0000
+++ duplicity-bin	2011-06-11 16:21:34 +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 ""
 
@@ -121,12 +131,23 @@
         log.Info("PASSPHRASE variable not set, asking user.")
         while 1:
             if n == 2:
-                pass1 = globals.gpg_profile.passphrase
-            else:
-                if globals.gpg_profile.passphrase:
+                if for_signing:
+                    pass1 = globals.gpg_profile.signing_passphrase
+                else:
                     pass1 = globals.gpg_profile.passphrase
+            else:
+                if for_signing:
+                    if globals.gpg_profile.signing_passphrase and n != 3:
+                        pass1 = globals.gpg_profile.signing_passphrase
+                    else:
+                        pass1 = getpass.getpass("GnuPG passphrase for signing key: ")
                 else:
-                    pass1 = getpass.getpass("GnuPG passphrase: ")
+                    # do not hang in an infinite loop if the first passphrase
+                    # was wrong, just ask again
+                    if globals.gpg_profile.passphrase and n != 3:
+                        pass1 = globals.gpg_profile.passphrase
+                    else:
+                        pass1 = getpass.getpass("GnuPG passphrase: ")
 
             if n == 1:
                 pass2 = pass1
@@ -953,6 +974,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)
@@ -1135,19 +1157,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()
 
@@ -1197,6 +1216,11 @@
 
     os.umask(077)
 
+    # the passphrase is not always needed for full/inc; symmetric crypto is another story
+    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":
@@ -1215,16 +1239,28 @@
         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 action == "full":
-            globals.gpg_profile.passphrase = get_passphrase(2, action)
+            if not globals.gpg_profile.sign_key:
+                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)
+                if not globals.gpg_profile.sign_key:
+                    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-04-04 15:50:11 +0000
+++ duplicity.1	2011-06-11 16:21:34 +0000
@@ -74,6 +74,13 @@
 passphrase to give to GnuPG.  If this is not set, the user will be
 prompted for the passphrase.
 
+When signing the backup with a GPG key as specified by
+.B --sign-key
+, the environment variable SIGN_PASSPHRASE can be used to set a
+passphrase for this 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.
+
 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 +306,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 .

=== modified file 'duplicity/commandline.py'
--- duplicity/commandline.py	2011-05-11 13:49:54 +0000
+++ duplicity/commandline.py	2011-06-11 16:21:34 +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-04-16 20:25:01 +0000
+++ duplicity/gpg.py	2011-06-11 16:21:34 +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,11 @@
         if profile.sign_key:
             gnupg.options.default_key = profile.sign_key
             cmdlist.append("--sign")
+        # the passphrase for --encrypt-key may not be the same as --sign-key
+        if encrypt and profile.recipients:
+            passphrase = profile.signing_passphrase
+        else:
+            passphrase = profile.passphrase
 
         if encrypt:
             if profile.recipients:
@@ -124,17 +131,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