1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
|
#!/usr/bin/env python
#******************************************************************************\
#* $Source: /u/blais/cvsroot/xxdiff/bin/xxdiff-encrypted,v $
#* $Id: xxdiff-encrypted,v 1.9 2004/02/25 17:37:35 blais Exp $
#* $Date: 2004/02/25 17:37:35 $
#*
#* Copyright (C) 2003-2004 Martin Blais <blais@furius.ca>
#*
#* 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 2 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, write to the Free Software
#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#*
#*****************************************************************************/
"""xxdiff-encrypted [<options>] <encrypted-file> [<encrypted file> ...]
Compare and merge contents of encrypted files relatively safely.
This script wraps around xxdiff, first decrypting the input files to temporary
files (for a short time) and running xxdiff on these files. There are two
typical uses of this program:
1) it is used to compare two encrypted files. With the --merge option, a
decision is required and an encrypted version of the merged file is output to
the specified file and the merged file deleted promptly. Note that without
the --merge option, even if the merged file is saved, it is deleted once
xxdiff exits.
2) it is used to split and resolve CVS conflicts in an armored encrypted file
(see --unmerge option). The merged file is encrypted and output over the
conflictual input file (i.e. it replaces it with the encrypted version of the
merged file). This is very useful if you maintain armored encrypted files in
CVS repositories because otherwise an encrypted file with a CVS conflict in
it becomes useless.
Using gpg-agent
---------------
Usage of this program with password caching using gpg-agent makes it much easier
to call on multiple files. The user's password given key is asked only once by
gpg-agent, kept in memory, and then decryption occurs without user intervention.
"""
__moredoc__ = """
Safety Notes
------------
The encrypted files are decrypted to temporary files for a short amount of time,
and are deleted when xxdiff appears. Note that their deletion is as safe as
Python's tempfile module allows it to be (in the author's opinion, safe
enough). I left comments in the code to allow a user to review where the files
are decrypted so they can judge by themselves if it is safe enough for their
use.
We could do much better in terms of safety if we could feed the input files to
xxdiff through different file descriptors (not impossible to implement) AND
calculate the diffs internally.
(Note that if someone can manipulate which program is used to actually perform
the diffs (e.g. modifying an unsuspecting user's resources in ~/.xxdiffrc), they
could feed the decrypted files to an arbitrary program.)
"""
__version__ = "$Revision: 1.9 $"
__author__ = "Martin Blais <blais@furius.ca>"
__depends__ = ['xxdiff', 'Python-2.3', 'GnuPG']
__copyright__ = """Copyright (C) 2003-2004 Martin Blais <blais@furius.ca>.
This code is distributed under the terms of the GNU General Public License."""
#===============================================================================
# EXTERNAL DECLARATIONS
#===============================================================================
import sys, os
from os.path import *
import re
from tempfile import NamedTemporaryFile
#===============================================================================
# LOCAL DECLARATIONS
#===============================================================================
pfx = join("%s.tmp." % basename(sys.argv[0]))
diffcmd = '%(xxdiff)s --merged-filename "%(merged)s" ' + \
'--indicate-input-processed ' # + filenames
# Note: we are not even using --decision for the case where output is requested
# (i.e. --output=filename), because we want to let the user decide at the time
# of comparison if he wants to back out on merging the files. Not letting him
# "not save" the merged output (i.e. forcing accept/merge/reject with
# --decision does give the option of the status quo.)
decodecmd = '%(gpg)s --decrypt --use-agent '
encodecmd_noarmor = '%(gpg)s --encrypt --use-agent '
encodecmd = encodecmd_noarmor + '--armor '
#-------------------------------------------------------------------------------
#
# Note: this is cut-n-paste from cvs.py, for simplicity of distribution.
def unmerge2( text ):
"""Unmerges conflicts between two files and returns the two split original
files."""
begre = re.compile('^<<<<<<< (.*)')
sepre = re.compile('^=======\s*$')
endre = re.compile('^>>>>>>> (.*)')
OUT, IN1, IN2 = 0, 1, 2
status = OUT
lines1, lines2 = [], []
for l in text.splitlines():
if status == OUT:
if begre.match(l):
status = IN1
else:
lines1.append(l)
lines2.append(l)
elif status == IN1:
if sepre.match(l):
status = IN2
else:
lines1.append(l)
elif status == IN2:
if endre.match(l):
status = OUT
else:
lines2.append(l)
text1 = os.linesep.join(lines1)
text2 = os.linesep.join(lines2)
return text1, text2
cvs_unmerge2 = unmerge2
#-------------------------------------------------------------------------------
#
def xxdiff( textlist, outmerged=None ):
"""Run a comparison of the encrypted texts specified in textlists and if
outmerged filename is specified, encrypt the merged file into it."""
# Create temporary files.
tempfiles = []
for t in xrange(len(textlist)):
f = NamedTemporaryFile(prefix=pfx)
print '== TEMPFILE', f.name
tempfiles.append(f)
# Always create a temporary file for the merged file, we will delete it for
# sure, since it would contain decrypted content if saved.
fmerge = NamedTemporaryFile(prefix=pfx)
print '== TEMPFILE', fmerge.name
m = {'xxdiff': opts.xxdiff,
'gpg': opts.gpg,
'merged': fmerge.name}
# Decode the files.
for i in xrange(len(textlist)):
t, f = textlist[i], tempfiles[i]
# Decode one file to an existing temporary file.
fin, fout = os.popen2(decodecmd % m, 'w')
fin.write(t)
fin.close()
decoded_output = fout.read()
fout.close()
f.write(decoded_output)
f.flush()
# Spawn xxdiff on the temporary/decoded files.
cmd = diffcmd % m + ' '.join(map(lambda x: '"%s"' % x.name, tempfiles))
fout = os.popen(cmd, 'r')
# Leave time for xxdiff to read the files before hiding, it will let us know
# when it is done with the input files.
l = fout.readline()
assert l.startswith('INPUT-PROCESSED')
# Close and automatically delete the temporary/decoded files.
for f in tempfiles:
f.close()
if opts.unmerge or opts.output:
print 'Waiting... save as merged file to merge back to encrypted file.'
else:
print 'Waiting...'
fout.read()
# Read the decoded merged output file from xxdiff.
textm = fmerge.read()
# Close and automatically delete the decoded merged output file.
fmerge.close()
if outmerged:
# Processed the merged text.
if not textm:
print >> sys.stderr, \
'Warning: merged file was not saved. Not replacing.'
else:
# Encode the merged output text.
if not opts.dont_armor:
cmd = encodecmd % m
else:
cmd = encodecmd_noarmor % m
if opts.recipient:
cmd += ' --recipient "%s"' % opts.recipient
fin, fout = os.popen2(cmd, 'w')
fin.write(textm)
fin.close()
encoded_output = fout.read()
fout.close()
# Write out the encoded output file.
try:
f = open(outmerged, 'w')
f.write(encoded_output)
f.close()
except IOError, e:
print >> sys.stderr, \
'Error: cannot write to encoded merged file.'
raise e
#-------------------------------------------------------------------------------
#
def complete( parser ):
"Programmable completion support. Script should work without it."
try:
import optcomplete
optcomplete.autocomplete(parser)
except ImportError:
pass
#===============================================================================
# MAIN
#===============================================================================
def main():
import optparse
parser = optparse.OptionParser(__doc__)
parser.add_option('-x', '--xxdiff', default="xxdiff",
help="specify path to xxdiff program to use")
parser.add_option('-g', '--gpg', default="gpg",
help="specify path to gpg program to use")
parser.add_option('-o', '--output', action='store',
help="require and encrypt merged output.")
parser.add_option('-u', '--unmerge', action='store_true',
help="split CVS conflicts in single input file and "
"encrypt required output merged file over input")
parser.add_option('-A', '--dont-armor', action='store_true',
help="Create output file in binary format.")
parser.add_option('-r', '--recipient', action='store',
help="Encrypt for user id name.")
global opts
complete(parser)
opts, args = parser.parse_args()
if not args:
raise parser.error('no files to decrypt and compare.')
if isabs(opts.xxdiff) and not exists(opts.xxdiff):
raise SystemExit('Error: xxdiff program does not exist in "%s"' %
opts.xxdiff)
if isabs(opts.gpg) and not exists(opts.gpg):
raise SystemExit('Error: gpg program does not exist in "%s"' %
opts.gpg)
if not (opts.unmerge or opts.output):
if opts.dont_armor:
print >> sys.stderr, "Warning: there will be no output file, " + \
"--dont-armor will means nothing special."
if opts.recipient:
print >> sys.stderr, "Warning: there will be no output file, " + \
"--recipient will means nothing special."
if opts.unmerge:
for fn in args: # do all files specified on cmdline, why not.
# Read input conflict file.
text = open(fn, 'r').read()
text1, text2 = cvs_unmerge2(text)
xxdiff([text1, text2], fn)
else:
if len(args) <= 1:
raise SystemExit("Error: you need to specify 2 or 3 arguments.")
textlist = []
for fn in args:
text = open(fn, 'r').read()
textlist.append(text)
xxdiff(textlist, opts.output)
if __name__ == '__main__':
main()
|