Den tis 23 feb. 2021 kl 17:46 skrev Daniel Shahaf <d...@daniel.shahaf.name>:

> Daniel Sahlberg wrote on Tue, Feb 23, 2021 at 16:50:07 +0100:
> > Den tis 23 feb. 2021 16:40Nathan Hartman <hartman.nat...@gmail.com>
> skrev:
> > > I think it's a good candidate for contrib (though it might be better
> > > to port it to portable Bourne shell first).
> > >
> > > Would a Python version be useful?
>
> Porting isa rewrite and as such may introduce bugs.
>
> Porting will increase the number of developers able to maintain the script.
>
> Porting may let some users run the script without installing zsh first.
> However, it's perhaps worth noting (in any eventual FAQ entry as well)
> that running the script doesn't require chsh(1)ing to zsh: there's
> nothing preventing bash users from installing zsh just so as to run
> scripts written in it.
>
> Porting wouldn't be a matter of translating line-by-line, since the
> «select»
> builtin (
> http://zsh.sourceforge.net/Doc/Release/Shell-Grammar.html#index-select)
> doesn't have a direct equivalent in other languages.  For translating to
> sh(1)
> specifically, the use of arrays would also be an issue.
>
> As to contrib/, even though it's deprecated, I don't have a better idea.
> (tools/ is probably not appropriate, unless more devs speak zsh than
> I know.)
>

Learning Python has been on my todo list for ages,  so I've cobbled
together something that seems to do the job.

There are basically two modes of operation:

./store-plaintext-password.py --listpassword|--list

Which lists all cached realms/usernames (and passwords). I've intentionally
split it in two in case someone prefer not to put the password on-screen,
but I've also added the option to display to show that the password is
really in plain text.

./store-plaintext-password.py realm password [username]

Which stores/updates the password for the given realm. If username is given
it is stored/updated as well. If there is no cached entry for the specified
realm a new file will be created (and username is a mandatory argument).

TODO:
- Is the license ok? I stole it from svn.c
- Improve Python code
- Improve documentation - especially on where to find the 'realm' string
- Decide where to put it - I've gone with contrib for now as per Nathan's
and Daniel's suggestions

> > Regarding the FAQ, currently we have [1] "Ahhh! I just discovered that
> > > my Subversion client is caching passwords in plain-text on disk!
> > > AHHH!" That is still applicable to 1.10, but now we need an entry to
> > > answer the opposite question: how to cache the password for svn use
> > > with cron jobs and non-X environments where Kwallet and GNOME-Keyring
> > > aren't applicable, and the particularly annoying case in which the
> > > machine itself has a GUI but the user is logged in via ssh; in this
> > > case the svn client will "freeze" while waiting for password entry in
> > > an inaccessible GUI window; I think this would occur with Kwallet,
> > > GNOME-Keyring, and macOS's Keychain.)
> > >
> > > But, as there doesn't seem to be one well-established way to handle
> > > this, other than just storing the password on disk, would the new FAQ
> > > entry say just that? Do we have any other concrete suggestions?
>
> If a cron job needs authentication, its credentials need to be stored
> somewhere, either in plaintext or in "as good as" plaintext.  I think
> storing the passwords in unobfuscated plaintext was a deliberate
> decision, informed by CVS's design choices in this regard, but I wasn't
> around in the early days.
>
> On the other hand, for GUI-less environments and for headful
> environments ssh'd into, we should simply document the capabilities and
> workarounds of the libraries we use (possibly by pointing to those
> libraries' documentations).
>

Suggestion for new FAQ entry:
[[[
Ahhh! I just discovered that my Subversion client is NOT caching passwords
in plain-text on disk! AHHH!
Calm down, take a deep breath.

This is the opposite of the previous question. After changing the compile
time default to not store passwords in plain-text there has been a number
of requests in the mailing lists to store the password.

If you understand the security implications, you have ruled out other
alternatives and still want to cache your password in plain-text on disk
you can use the script
https://svn.apache.org/repos/asf/subversion/trunk/contrib/client-side/store-plaintext-password-py
to store the password in the directory which contains the cached passwords
(usually ~/.subversion/auth/). The script can also be used to list any
existing passwords stored in plain-text.
]]]

I'm also suggesting to change the existing FAQ entry (Ahhh! I just
discovered that my Subversion client is caching passwords in plain-text on
disk! AHHH!) to mention the changed compile time default since 1.12 to not
store plain-text passwords:

[[[
s/Otherwise, the client will fall back/Otherwise, the client can fall back/

Since svn 1.12 the compile time default has been to disable storing new
passwords in plain-text, but old passwords can still be used. Certain
distributions may also have selected to use the compile time option to
enable plain-text password storage.

s/However .*/In case Subversion is compiled with support for storing
plain-text passwords, you can disable it in your run-time config file by
setting 'store-plaintext-passwords = no' (so that encrypted stores like
GNOME Keyring and KWallet will still be used - this is already done in at
least one distribution which has selected to enable the plain-text password
storage in svn 1.12). If you want to disable storing any kind of
credentials you may instead set 'store-auth-creds = no', or you can use the
more narrowly-defined 'store-passwords = no' (so that server certs are
still cached). More information on password cacheing is in chapter 6 of the
"Nightly Build" Subversion book, under "Client Credentials Caching"./
]]]

The "Since svn 1.12..." should probably go in the end of the first "On
UNIX/Linux" section, after "(Since svn 1.6.)"

> As for the script, IIRC there was a need for the username (?) to be cached
> > before running the script. Where should that be stored?
>
> In the md5(realm)-named file in ~/.subversion/auth/ which the script
> ed(1)s.  (The --no-auth-cache flag and the store-auth-creds
> configuration knob may affect whether the username is saved to disk.)
>
> The easiest way to populate that is to run `svn info` on the URL and
> authenticate manually.
>
> A script that takes a URL/username/password and inserts that tuple into
> the cache would be nice, of course, but it'd need to compute the realm
> string of the URL.
>

Thanks for the info, that helped me a lot in crafting the script. I've gone
the easy way and required the user to provide the realm string.

Kind regards,
Daniel Sahlberg
Index: contrib/client-side/store-plaintext-password.py
===================================================================
--- contrib/client-side/store-plaintext-password.py     (nonexistent)
+++ contrib/client-side/store-plaintext-password.py     (working copy)
@@ -0,0 +1,207 @@
+#!/usr/bin/env python3
+
+# Script to store password in plaintext in ~/.subversion/auth/svn.simple/
+#
+# Script require the tabulate module: pip install tabulate
+#
+# ====================================================================
+#    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.
+# ====================================================================
+
+from hashlib import md5
+import argparse
+import os
+from os import listdir
+from os.path import isfile, join
+from pathlib import Path
+from tabulate import tabulate
+
+# Read a hashfile and return a key/value list
+#
+# A hashfile should contain the following four lines, repeted as needed, 
ending by the line END
+# K [length]
+# [keyname]
+# V [length]
+# [value]
+# ...
+# END
+#
+# The length is not validated
+def readHash(file):
+    # Read K [length] or END but discard it
+    key = file.readline().strip()
+    if key == 'END':
+        return None
+    if key[:1] != 'K':
+        raise Exception('Parse failed, expected K...')
+
+    # Read keyname
+    key = file.readline().strip()
+
+    # Read V [length] but discard it
+    val = file.readline().strip()
+    if val[:1] != 'V':
+        raise Exception('Parse failed, expected V...')
+
+    # Read value
+    val = file.readline().strip()
+
+    return [key, val]
+
+# Write a key/value pair to a hashfile
+def writeHash(file, key, val):
+    file.write('K ' + str(len(key)) + '\n')
+    file.write(key + '\n')
+    file.write('V ' + str(len(val)) + '\n')
+    file.write(val + '\n')
+
+# Main function
+parser = argparse.ArgumentParser(description='Store plain text password in 
~/.subversion/auth/svn.simple/')
+parser.add_argument('realm', help='Realm string from server, required value 
for updates', nargs='?')
+parser.add_argument('password', help='Password, required value for updates', 
nargs='?')
+parser.add_argument('username', help='Username, optional value for updates', 
nargs='?')
+parser.add_argument('--list', help='Lists all stored realms/usernames', 
action='store_true')
+parser.add_argument('--listpassword', help='Lists all stored 
realms/usernames/passwords', action='store_true')
+
+args = parser.parse_args()
+authpath = str(Path.home()) + '/.subversion/auth/svn.simple/'
+
+if args.listpassword or args.list:
+    data = []
+    if args.listpassword:
+        header = ['Realm', 'Username', 'Password']
+    else:
+        header = ['Realm', 'Username']
+    for name in listdir(authpath):
+        fullname = join(authpath, name)
+        if isfile(fullname):
+            file = open(fullname, 'r')
+            realm = ''
+            user = ''
+            password = ''
+            while(True):
+                hash = readHash(file)
+                if hash is None:
+                    break
+                elif hash[0] == 'svn:realmstring':
+                    realm = hash[1]
+                elif hash[0] == 'username':
+                    user = hash[1]
+                elif hash[0] == 'password':
+                    password = hash[1]
+            file.close()
+            if args.listpassword:
+                data.append([realm, user, password])
+            else:
+                data.append([realm, user])
+    print(tabulate(data, headers=header))
+    quit()
+    
+if args.realm is None:
+    print("Realm required\n")
+    parser.print_help()
+    quit()
+
+if args.password is None:
+    print("Password required\n")
+    parser.print_help()
+    quit()
+
+# The file name is the md5encoding of the realm
+m = md5()
+m.update(args.realm.encode('utf-8'))
+fullname = join(authpath, m.hexdigest())
+
+# In an existing file, we add/replace password/username/passtype
+if isfile(fullname):
+    pwdAdded = False
+    passtypeAdded = False
+    usernameAdded = False
+    
+    # Read existing file, write to .tmp which we rename in the end
+    inFile = open(fullname, 'r')
+    outFile = open(fullname + '.tmp', 'w')
+    while(True):
+        # Read K [length] or END and write back
+        line = inFile.readline()
+        if not line:
+            raise Exception('Parse failed, expected K ... or END')
+        if line.strip() == 'END':
+            # If username, password and/or passtype has not been found in the 
file, add them here
+            if not usernameAdded and args.username is not None:
+                writeHash(outFile, 'username', args.username)
+            if not passtypeAdded:
+                writeHash(outFile, 'passtype', 'simple')
+            if not pwdAdded:
+                writeHash(outFile, 'password', args.password)
+            outFile.write(line)
+            break
+        outFile.write(line)
+        
+        # Read keyname and write back, save keyname for later
+        line = inFile.readline()
+        outFile.write(line)
+        key = line.strip()
+        
+        # Read V [length] and write back, possibly updating 
username/password/passtype lengths
+        line = inFile.readline()
+        if not line:
+            raise Exception('Parse failed, expected V ...')
+        if key == 'username' and args.username is not None:
+            outFile.write('V ' + str(len(args.username)) + '\n')
+        elif key == 'passtype':
+            outFile.write('V 6\n')
+        elif key == 'password':
+            outFile.write('V ' + str(len(args.password)) + '\n')
+        else:
+            outFile.write(line)
+            
+        # Read value and write back, possibly updating 
username/password/passtype
+        line = inFile.readline()
+        if key == 'username' and args.username is not None:
+            outFile.write(args.username + '\n')
+            usernameAdded = True
+        elif key == 'passtype':
+            outFile.write('simple\n')
+            passtypeAdded = True
+        elif key == 'password':
+            outFile.write(args.password + '\n')
+            pwdAdded = True
+        else:
+            outFile.write(line)
+            
+    # Close and move the .tmp file in place
+    inFile.close()
+    outFile.close()
+    os.rename(fullname + '.tmp', fullname)
+    
+# File doesn't exists, write new one from scratch
+else:
+    if args.username is None:
+        print("New file: Username required\n")
+        parser.print_help()
+        quit()
+        
+    outFile = open(fullname, 'w')
+    writeHash(outFile, 'svn:realmstring', args.realm)
+    writeHash(outFile, 'username', args.username)
+    writeHash(outFile, 'passtype', 'simple')
+    writeHash(outFile, 'password', args.password)
+    outFile.write('END\n')
+    outFile.close()

Property changes on: contrib/client-side/store-plaintext-password.py
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property

Reply via email to