Skip to content

Instantly share code, notes, and snippets.

@mprymek
Last active February 25, 2020 14:06
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save mprymek/10415576 to your computer and use it in GitHub Desktop.
Save mprymek/10415576 to your computer and use it in GitHub Desktop.
encfs-agent - mount encfs filesystems using ssh-agent
#! /usr/bin/env python2
"""
encfs-agent
Mounts encfs filesystems with passwords generated using private keys stored in ssh-agent.
You can have any number of encrypted filesystems ("vaults") under VAULTS_DIR. Password for
each of them is derived from its name and given private key stored in ssh-agent.
You can use ssh-askpass for ssh-agent if you want.
Adapted from code from http://ptspts.blogspot.cz/2010/06/how-to-use-ssh-agent-programmatically.html
Thank you, pts!
------------------ DISCLAIMER
Please note I am not an cryptography expert at all. It's up to you to decide if the algorithm used
is suitable for your needs.
See e.g. https://crypto.stackexchange.com/questions/19631/is-it-safe-to-derive-a-password-from-a-signature-provided-by-ssh-agent
------------------ LICENSE
Copyright 2020 Miroslav Prymek
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------ REQUIREMENTS
- python 2.x
- correctly configured ssh-agent (test with "ssh-add -L")
------------------ INSTALL
## download encfs-agent file and adjust VAULTS_DIR and MNT_DIR variables in it if you don't like the defaults
## defaults are:
## vaults stored in ~/.vaults
## vaults mounted under /mnt/vaults
## Ready! You can test it...
## set following variables appropriately (use the same values as in the previous step)
EA=~/bin/encfs-agent
VAULTS_DIR=~/.vaults
MNT_DIR=/mnt/vaults
chmod +x "$EA"
mkdir -p "$MNT_DIR"
mkdir -p "$VAULTS_DIR"
## create first vault...
$EA mount first
-- encfs will prompt you for some settings for a newly created fs...
echo test > $MNT_DIR/first/test
umount $MNT_DIR/first
## see the encrypted directory...
ls -la "$VAULTS_DIR/first"
## mount it again...
$EA mount first
cat $MNT_DIR/first/test
umount $MNT_DIR/first
## and again...
$EA passwd first | encfs --extpass='cat' "$VAULTS_DIR/first" "$MNT_DIR/first"
cat /mnt/vaults/first/test
umount /mnt/vaults/first
------------------ NOTE
ssh-agent client sends arbitrary string to the agent. Agent makes SHA-1 hash of it and encrypts the
hash with a chosen private key. SHA-1 hashes are 160 bit long, so we can get only precisely
160 bits long passwords.
"""
import cStringIO
import os
import os.path
import re
import sha
import socket
import struct
import sys
import subprocess
# adjust this for your system...
VAULTS_DIR=os.path.join(os.getenv('HOME'),'.vaults')
MNT_DIR="/mnt/vaults"
# no user serviceable parts below this line
SSH2_AGENTC_REQUEST_IDENTITIES = 11
SSH2_AGENT_IDENTITIES_ANSWER = 12
SSH2_AGENTC_SIGN_REQUEST = 13
SSH2_AGENT_SIGN_RESPONSE = 14
SSH_AGENT_FAILURE = 5
def recv_all(sock, size):
if size == 0:
return ''
assert size >= 0
if hasattr(sock, 'recv'):
recv = sock.recv
else:
recv = sock.read
data = recv(size)
if len(data) >= size:
return data
assert data, 'unexpected EOF'
output = [data]
size -= len(data)
while size > 0:
output.append(recv(size))
assert output[-1], 'unexpected EOF'
size -= len(output[-1])
return ''.join(output)
def recv_u32(sock):
return struct.unpack('>L', recv_all(sock, 4))[0]
def recv_str(sock):
return recv_all(sock, recv_u32(sock))
def append_str(ary, data):
assert isinstance(data, str)
ary.append(struct.pack('>L', len(data)))
ary.append(data)
# Get list of public keys, and find our key.
def get_pubkey(sock,key_comment):
sock.sendall('\0\0\0\1\v') # SSH2_AGENTC_REQUEST_IDENTITIES
response = recv_str(sock)
resf = cStringIO.StringIO(response)
assert recv_all(resf, 1) == chr(SSH2_AGENT_IDENTITIES_ANSWER)
num_keys = recv_u32(resf)
assert num_keys < 2000 # A quick sanity check.
assert num_keys, 'no keys have_been added to ssh-agent'
matching_keys = []
for i in xrange(num_keys):
key = recv_str(resf)
comment = recv_str(resf)
if comment == key_comment:
matching_keys.append(key)
assert '' == resf.read(1), 'EOF expected in resf'
assert matching_keys, 'no keys in ssh-agent with comment %r' % key_comment
assert len(matching_keys) == 1, (
'multiple keys in ssh-agent with comment %r' % key_comment)
assert matching_keys[0].startswith('\x00\x00\x00\x07ssh-rsa\x00\x00'), (
'non-RSA key in ssh-agent with comment %r' % key_comment)
keyf = cStringIO.StringIO(matching_keys[0][11:])
public_exponent = int(recv_str(keyf).encode('hex'), 16)
modulus_str = recv_str(keyf)
modulus = int(modulus_str.encode('hex'), 16)
assert '' == keyf.read(1), 'EOF expected in keyf'
return (matching_keys[0],public_exponent,modulus,len(modulus_str))
# Ask ssh-agent to sign with our key.
def sign(sock,pub_key,msg_to_sign):
request_output = [chr(SSH2_AGENTC_SIGN_REQUEST)]
append_str(request_output, pub_key)
append_str(request_output, msg_to_sign)
request_output.append(struct.pack('>L', 0)) # flags == 0
full_request_output = []
append_str(full_request_output, ''.join(request_output))
full_request_str = ''.join(full_request_output)
sock.sendall(full_request_str)
response = recv_str(sock)
resf = cStringIO.StringIO(response)
assert recv_all(resf, 1) == chr(SSH2_AGENT_SIGN_RESPONSE)
signature = recv_str(resf)
assert '' == resf.read(1), 'EOF expected in resf'
assert signature.startswith('\0\0\0\7ssh-rsa\0\0')
sigf = cStringIO.StringIO(signature[11:])
signed_value = int(recv_str(sigf).encode('hex'), 16)
assert '' == sigf.read(1), 'EOF expected in sigf'
return signed_value
# Verify the signature.
def verify_signature(signed_value,pub_exponent,pub_modulus,pub_modlen,msg_to_sign):
decoded_value = pow(signed_value, pub_exponent, pub_modulus)
decoded_hex = '%x' % decoded_value
if len(decoded_hex) & 1:
decoded_hex = '0' + decoded_hex
decoded_str = decoded_hex.decode('hex')
assert len(decoded_str) == pub_modlen - 2 # e.g. (255, 257)
assert re.match(r'\x01\xFF+\Z', decoded_str[:-36]), 'bad padding found'
expected_sha1_hex = decoded_hex[-40:]
msg_sha1_hex = sha.sha(msg_to_sign).hexdigest()
return expected_sha1_hex == msg_sha1_hex
def usage():
print """
{0} converts any string to 160-bit password using private key stored in ssh-agent.
This password is used to conveniently mount encrypted encfs filesystems.
usage:
{0} mount <vault_name> [ssh_key_file]
-- mount encfs {1}/<vault_name> to {2}/<vault_name>
{0} passwd <vault_name> [ssh_key_file]
-- convert vault_name (or arbitrary string) to password
vault_name ... encfs will mount {1}/<vault_name> to {2}/<vault_name>
ssh_key_file ... key file name (key comment as printed by "ssh-add -L")
""".format(sys.argv[0],VAULTS_DIR,MNT_DIR)
sys.exit(1)
def main():
if len(sys.argv)<3:
usage()
key_comment = os.path.join(os.getenv('HOME'),'.ssh','id_rsa')
if len(sys.argv)==4:
key_comment = sys.argv[3]
vault = sys.argv[2]
if sys.argv[1]=='passwd':
# Connect to ssh-agent.
assert 'SSH_AUTH_SOCK' in os.environ, (
'ssh-agent not found, set SSH_AUTH_SOCK')
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0)
sock.connect(os.getenv('SSH_AUTH_SOCK'))
pub_key,pub_exponent,pub_modulus,pub_modlen = get_pubkey(sock,key_comment)
passwd = sign(sock,pub_key,vault)
assert verify_signature(passwd,pub_exponent,pub_modulus,pub_modlen,vault), 'bad signature (SHA1 mismatch)'
#print "vault=%r password=%r"%(vault,passwd)
sys.stdout.write("%x"%(passwd,))
elif sys.argv[1]=='mount':
mnt_src=os.path.join(VAULTS_DIR,vault)
mnt_dest=os.path.join(MNT_DIR,vault)
## we could pass password by "-extpass=cat", but that will break interaction
## with encfs (at first mount, when creating encrypted fs)
cmd=["encfs", "--extpass='%s' passwd '%s'"%(sys.argv[0],vault),mnt_src,mnt_dest]
#print '"'+('" "'.join(cmd))+'"'
subprocess.call(cmd)
else:
usage()
sys.exit(0)
if __name__ == '__main__':
main()
@berlincount
Copy link

Uhm. How does this offer any security?

Essentially, a SHA1-sum derived directly from the vault name is used as a password. Wouldn't even need to talk to the ssh-agent

echo -n vault | sha1sum

@mprymek
Copy link
Author

mprymek commented Dec 30, 2015

@berlincount: I don't understand your comment. The hash SIGNED by ssh-agent is used as a password.

https://gist.github.com/mprymek/10415576#file-encfs-agent-L217

@tibolpol
Copy link

tibolpol commented May 27, 2018

Thx a lot for the passwd function, the good idea.
The mount function needs a patch :
https://gist.github.com/mprymek/10415576#file-encfs-agent-L226 must be
cmd=["encfs", "--extpass='%s' passwd '%s' '%s'"%(sys.argv[0],vault,key_comment),mnt_src,mnt_dest]

Also useful : https://github.com/jwhitham/safeu (ssh agent encryption utility + ssh agent locator)

And of course : https://github.com/funtoo/keychain (frontend to ssh-agent and ssh-add, but allows you to easily have one long running ssh-agent process per system, rather than the norm of one ssh-agent per login session)

@AlexeySalmin
Copy link

AlexeySalmin commented Dec 14, 2019

Hi, Prýmek! @mprymek

There're no pull-requests on gist, so I've forked your code into a repository: https://github.com/AlexeySalmin/encfs-agent

Would you mind making an explicit choice of license for this particular code snippet? (e.g. MIT). I'd be happy to include it into the repo since the copyright is yours.

@mprymek
Copy link
Author

mprymek commented Feb 6, 2020

@AlexeySalmin I'm very sorry I've missed your comment. I've added a disclaimer and MIT license.

@AlexeySalmin
Copy link

@AlexeySalmin I'm very sorry I've missed your comment. I've added a disclaimer and MIT license.

Thanks! I've ported that commit into my fork:
AlexeySalmin/encfs-agent@504f68d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment