| #!/usr/bin/env python3 |
| """\ |
| Script to store password in plaintext in ~/.subversion/auth/svn.simple/ |
| |
| Useful in case Subversion is compiled without support for writing |
| passwords in plaintext. |
| |
| Only use this script if the security implications are understood |
| and it is acceptable by your organization to store passwords in plaintext. |
| |
| See http://subversion.apache.org/faq.html#plaintext-passwords |
| """ |
| |
| # ==================================================================== |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # ==================================================================== |
| |
| import os |
| import sys |
| |
| TERMINATOR = b"END\n" |
| |
| PARSERDESCR = """\ |
| Store plaintext password in ~/.subversion/auth/svn.simple/ |
| |
| Existing passwords and authentication realms can be inspected by: |
| |
| svn auth [--show-passwords] |
| |
| The authentication realm can also be found using: |
| |
| svn info URL |
| """ |
| |
| def _read_one_datum(fd, letter): |
| """\ |
| Read a 'K <length>\\n<key>\\n' or 'V <length>\\n<value>\\n' block from |
| an svn_hash_write2()-format FD. |
| |
| LETTER identifies the first letter, as a bytes object. |
| """ |
| assert letter in {b'K', b'V'} |
| |
| # Read the letter and the space |
| readletter = fd.read(1) |
| if readletter != letter or fd.read(1) != b' ': |
| raise ValueError('Hash file format error: Expected {} got {}'.format(letter, readletter)) |
| |
| # Read the length and the newline |
| line = fd.readline() |
| if line[-1:] != b'\n': |
| raise ValueError('Hash file format error: Expected trailing \\n') |
| expected_length = int(line[:-1]) |
| |
| # Read the datum and its newline |
| datum = fd.read(expected_length) |
| if len(datum) != expected_length: |
| raise ValueError('Hash file format error: Expected length {} got {}'.format(expected_length, len(datum))) |
| if fd.read(1) != b'\n': |
| raise ValueError('Hash file format error: Extra data after reading {} bytes, expected \\n') |
| |
| return datum |
| |
| # Our version of svn_hash_read2(), named without "svn_" prefix to avoid |
| # potential naming conflicts with stuff star-imported from svn.core. |
| def hash_read(fd): |
| """\ |
| Read an svn_hash_write2()-formatted file from FD, terminated by "END". |
| |
| Return a dict mapping bytes to bytes. |
| """ |
| assert 'b' in fd.mode |
| assert TERMINATOR[0] not in {b'K', b'V'} |
| |
| ret = {} |
| while True: |
| if fd.peek(1)[0] == TERMINATOR[0]: |
| if fd.readline() != TERMINATOR: |
| raise ValueError('Hash file format error: Expected file terminator {}'.format(TERMINATOR)) |
| if fd.peek(1): |
| raise ValueError('Hash file format error: Extra content after file terminator') |
| return ret |
| |
| key = _read_one_datum(fd, b'K') |
| value = _read_one_datum(fd, b'V') |
| ret[key] = value |
| |
| def outputHash(fd, hash): |
| """\ |
| Write a dictionary HASH to an open file descriptor FD in the |
| svn_hash_write2()-format, terminated by "END\\n". |
| |
| The keys and values must have datatype 'bytes' and strings must be |
| encoded using utf-8. |
| """ |
| assert 'b' in fd.mode |
| |
| for key, val in dict.items(): |
| fd.write(b'K ' + bytes(str(len(key)), 'utf-8') + b'\n') |
| fd.write(key + b'\n') |
| fd.write(b'V ' + bytes(str(len(val)), 'utf-8') + b'\n') |
| fd.write(val + b'\n') |
| fd.write(TERMINATOR) |
| |
| def writeHashFile(filename, hash): |
| """\ |
| Write the dict HASH to a file named FILENAME in svn_hash_write2() |
| format. |
| """ |
| tmpFilename = filename + '.tmp' |
| try: |
| with open(tmpFilename, 'xb') as fd: |
| outputHash(fd, dict) |
| os.rename(tmpFilename, filename) |
| except FileExistsError: |
| print('{}: File {!r} already exist. Is the script already running?' |
| .format(os.path.basename(__file__), tmpFilename), |
| file=sys.stderr) |
| except: |
| os.remove(tmpFilename) |
| raise |
| |
| def main(): |
| # These imports are only being used by main |
| import argparse |
| import getpass |
| import hashlib |
| |
| # Parse arguments |
| parser = argparse.ArgumentParser( |
| description=PARSERDESCR, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| parser.add_argument('realm', help='Server authentication realm') |
| parser.add_argument('-u', '--user', help='Set username') |
| args = parser.parse_args() |
| |
| # The file name is the md5encoding of the realm |
| m = hashlib.md5() |
| m.update(args.realm.encode('utf-8')) |
| authfileName = os.path.join(os.path.expanduser('~/.subversion/auth/svn.simple/'), m.hexdigest()) |
| |
| # If the authfile doesn't already exist, verify that a username has been provided |
| # or else prompt for it |
| existingFile = os.path.exists(authfileName) |
| if not existingFile and args.user is None: |
| args.user = input("Enter username for realm {}: ".format(args.realm)) |
| if args.user == '': |
| parser.exit(1, 'Username required.\n') |
| |
| # Prompt for password |
| password = getpass.getpass("Enter password for realm {}: ".format(args.realm)) |
| |
| # In an existing file, we add/replace password/username/passtype |
| if existingFile: |
| hash = hash_read(open(authfileName, 'rb')) |
| if args.user is not None: |
| hash[b'username'] = args.user.encode('utf-8') |
| hash[b'password'] = password.encode('utf-8') |
| hash[b'passtype'] = b'simple' |
| |
| # For a new file, set realmstring, username, password and passtype |
| else: |
| hash = { |
| b'svn:realmstring': args.realm.encode('utf-8'), |
| b'username': args.user.encode('utf-8'), |
| b'passtype': b'simple', |
| b'password': password.encode('utf-8'), |
| } |
| |
| del password |
| |
| # Write out the resulting file |
| writeHashFile(authfileName, hash) |
| |
| |
| if __name__ == '__main__': |
| main() |
| |