Skip to content

Commit

Permalink
Merge 4c92507 into 7b633e0
Browse files Browse the repository at this point in the history
  • Loading branch information
prabi authored Aug 29, 2020
2 parents 7b633e0 + 4c92507 commit 5cc58cf
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 70 deletions.
22 changes: 11 additions & 11 deletions pypass/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ def generate(config, pass_name, pass_length, no_symbols, clip, in_place):
@click.argument('path', type=click.STRING)
def edit(config, path):
if path in config['password_store'].get_passwords_list():
old_password = config['password_store'].get_decrypted_password(path)
old_password = config['password_store']\
.get_decrypted_password(path).content
with tempfile.NamedTemporaryFile() as temp_file:
temp_file.write(old_password.encode())
temp_file.flush()
Expand Down Expand Up @@ -207,8 +208,7 @@ def show(config, path, clip):
click.echo('Error: %s is not in the password store.' % path)
sys.exit(1)

decrypted_password = \
config['password_store'].get_decrypted_password(path).strip()
entry = config['password_store'].get_decrypted_password(path)

if clip:
xclip = subprocess.Popen(
Expand All @@ -218,21 +218,21 @@ def show(config, path, clip):
],
stdin=subprocess.PIPE
)
xclip.stdin.write(decrypted_password.split('\n')[0].encode('utf8'))
xclip.stdin.write(entry.password.encode('utf8'))
xclip.stdin.close()
click.echo('Copied %s to clipboard.' % path)
else:
click.echo(decrypted_password)
click.echo(entry.content.rstrip())


@main.command()
@click.argument('path', type=click.STRING)
@click.pass_obj
def connect(config, path):
store = config['password_store']
hostname = store.get_decrypted_password(path, entry=EntryType.hostname)
username = store.get_decrypted_password(path, entry=EntryType.username)
password = store.get_decrypted_password(path, entry=EntryType.password)
entry = config['password_store'].get_decrypted_password(path)
hostname = entry[EntryType.hostname]
username = entry[EntryType.username]
password = entry[EntryType.password]
s = pxssh.pxssh()
click.echo("Connectig to %s" % hostname)
s.login(hostname, username, password=password)
Expand Down Expand Up @@ -309,8 +309,8 @@ def find(config, search_terms):
@click.pass_obj
def grep(config, search_string):
for password in config['password_store'].get_passwords_list():
decrypted_password = \
config['password_store'].get_decrypted_password(password)
decrypted_password = config['password_store']\
.get_decrypted_password(password).content

grep = subprocess.Popen(
['grep', '-e', search_string],
Expand Down
71 changes: 71 additions & 0 deletions pypass/password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#
# Copyright (C) 2014 Alexandre Viau <alexandre@alexandreviau.net>
# Copyright (C) 2020 Peter Rabi <peter.rabi@gmail.com>
#
# This file is part of python-pass.
#
# python-pass 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 3 of the License, or
# (at your option) any later version.
#
# python-pass 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 python-pass. If not, see <http://www.gnu.org/licenses/>.
#

import re

from .entry_type import EntryType


class Password:
"""Password is a decoded Password Store entry.
It has two main `str` attributes:
* `content`, which is the complete decoded text of the entry, and
* `password`, which is either
* the string following "pass: " or "password: " until \\n or EOF, or
* the first line (without \\n).
Content of specific `EntryType` can be retrieved by using
`password_object[EntryType.enum_member]` syntax.
"""

def __init__(self, content):
self.content = content
pw = re.search('(?:password|pass): (.+)', content)
if pw is not None:
self.password = pw.group(1)
else: # If there is no match, password is the first line
self.password = content.partition('\n')[0]

def __getitem__(self, key):
"""Get the value from a "key: value" formatted line.
:param key: The key, that is an `EntryType` enum member.
:returns: The `str` value corresponding to the given `key`.
`None`, if the `key` wasn't found.
"""
if not isinstance(key, EntryType):
raise TypeError(
'Password objects can only retrieve EntryType values.'
)
if key is EntryType.password:
return self.password
elif key is EntryType.username:
usr = re.search('(?:username|user|login): (.+)', self.content)
if usr is not None:
return usr.group(1)
elif key is EntryType.hostname:
hostname = re.search('(?:host|hostname): (.+)', self.content)
if hostname is not None:
return hostname.group(1)
33 changes: 5 additions & 28 deletions pypass/passwordstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
import os
import subprocess
import string
import re

from .entry_type import EntryType
from .password import Password

# Secure source of randomness for password generation
try:
Expand Down Expand Up @@ -115,12 +114,11 @@ def get_passwords_list(self):

return passwords

def get_decrypted_password(self, path, entry=None):
"""Returns the content of the decrypted password file
def get_decrypted_password(self, path):
"""Returns the decrypted password file as a `Password` object
:param path: The path of the password to be decrypted. Example:
'email.com'
:param entry: The entry to retreive. (EntryType enum)
"""
passfile_path = os.path.realpath(
os.path.join(
Expand All @@ -144,28 +142,7 @@ def get_decrypted_password(self, path, entry=None):

if gpg.returncode == 0:
decrypted_password = gpg.stdout.read().decode()

if entry == EntryType.username:
usr = re.search(
'(?:username|user|login): (.+)',
decrypted_password
)
if usr:
return usr.groups()[0]
elif entry == EntryType.password:
pw = re.search('(?:password|pass): (.+)', decrypted_password)
if pw:
return pw.groups()[0]
else: # If there is no match, password is the first line
return decrypted_password.split('\n')[0]
elif entry == EntryType.hostname:
hostname = re.search(
'(?:host|hostname): (.+)', decrypted_password
)
if hostname:
return hostname.groups()[0]
else:
return decrypted_password
return Password(decrypted_password)
else:
raise Exception('Couldn\'t decrypt %s' % path)

Expand Down Expand Up @@ -220,7 +197,7 @@ def generate_password(
:returns: Generated password.
"""
if first_line_only:
old_content = self.get_decrypted_password(path)
old_content = self.get_decrypted_password(path).content
content_wo_pass = ''.join(old_content.partition('\n')[1:])
else:
content_wo_pass = ''
Expand Down
10 changes: 5 additions & 5 deletions pypass/tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def test_insert(self):
self.run_cli(['insert', '-m', 'test.com'], input='first\nsecond\n')

store = PasswordStore(self.dir)
content = store.get_decrypted_password('test.com')
content = store.get_decrypted_password('test.com').content
self.assertEqual(content, 'first\nsecond\n')

# Echo the password and ask for it only once
Expand All @@ -120,7 +120,7 @@ def test_insert(self):
'Enter password for test2.com: oneLine\n'
)

content2 = store.get_decrypted_password('test2.com')
content2 = store.get_decrypted_password('test2.com').content
self.assertEqual(content2, 'oneLine')

# Mismatching inputs should cause abort
Expand Down Expand Up @@ -187,7 +187,7 @@ def test_edit(self):
mock_editor = os.path.join(os.path.dirname(__file__), 'mock_editor.py')
self.run_cli(['--EDITOR', mock_editor, 'edit', 'test.com'])

edited_content = store.get_decrypted_password('test.com')
edited_content = store.get_decrypted_password('test.com').content
self.assertEqual(edited_content, 'edited')

def test_edit_not_exist(self):
Expand Down Expand Up @@ -484,7 +484,7 @@ def test_generate_no_symbols(self):
self.assertIsNotNone(re.match('[a-zA-Z0-9]{25}$', password))

store = PasswordStore(self.dir)
decoded = store.get_decrypted_password('test.com')
decoded = store.get_decrypted_password('test.com').content
self.assertEqual(decoded, password)

def test_generate_in_place(self):
Expand All @@ -504,7 +504,7 @@ def test_generate_in_place(self):
'Replace generated password for in-place.com.'
)

new_content = store.get_decrypted_password('in-place.com')
new_content = store.get_decrypted_password('in-place.com').content
new_password, _, remainder = new_content.partition('\n')
self.assertEqual(len(new_password), 10)
self.assertEqual(remainder, 'second')
Expand Down
48 changes: 22 additions & 26 deletions pypass/tests/test_passwordstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,47 +96,43 @@ def test_encrypt_decrypt(self):

self.assertEqual(
password,
store.get_decrypted_password('hello.com')
store.get_decrypted_password('hello.com').content
)

def test_get_decrypted_password_specific_entry(self):
store = PasswordStore(self.dir)
password = 'ELLO'
store.insert_password('hello.com', password)
store.insert_password('hello.com', 'ELLO')
password = store.get_decrypted_password('hello.com')

# Using an `str` key to get an entry should fail.
self.assertRaises(TypeError, password.__getitem__, 'password')

# When there is no 'password:' mention, the password is assumed to be
# the first line.
self.assertEqual(
'ELLO',
store.get_decrypted_password('hello.com', entry=EntryType.password)
)
self.assertEqual('ELLO', password[EntryType.password])

store.insert_password('hello.com', 'sdfsdf\npassword: pwd')
self.assertEqual(
'pwd',
store.get_decrypted_password('hello.com', entry=EntryType.password)
)
password2 = store.get_decrypted_password('hello.com')
self.assertEqual('pwd', password2[EntryType.password])

# Getting a nonexistent entry should return `None`.
self.assertIsNone(password2[EntryType.username])

store.insert_password(
'hello',
'sdf\npassword: pwd\nusername: bob\nhost: salut.fr'
)
self.assertEqual(
'bob',
store.get_decrypted_password('hello', entry=EntryType.username)
)
self.assertEqual(
'salut.fr',
store.get_decrypted_password('hello', entry=EntryType.hostname)
)
password3 = store.get_decrypted_password('hello')
self.assertEqual('bob', password3[EntryType.username])
self.assertEqual('salut.fr', password3[EntryType.hostname])

def test_get_decrypted_password_only_password(self):
store = PasswordStore(self.dir)
password = 'ELLO'
store.insert_password('hello.com', password)
self.assertEqual(
'ELLO',
store.get_decrypted_password('hello.com')
store.get_decrypted_password('hello.com').content
)

def test_get_decrypted_password_deeply_nested(self):
Expand All @@ -148,11 +144,11 @@ def test_get_decrypted_password_deeply_nested(self):
store.insert_password('A/B/C/hello.com', 'Bob')
self.assertEqual(
'Alice',
store.get_decrypted_password('A/B/C/D/hello.com')
store.get_decrypted_password('A/B/C/D/hello.com').content
)
self.assertEqual(
'Bob',
store.get_decrypted_password('A/B/C/hello.com')
store.get_decrypted_password('A/B/C/hello.com').content
)
self.assertTrue(
os.path.isdir(os.path.join(self.dir, 'A', 'B', 'C', 'D'))
Expand Down Expand Up @@ -253,17 +249,17 @@ def test_generate_password(self):
store = PasswordStore(self.dir)

store.generate_password('letters.net', digits=False, symbols=False)
only_letters = store.get_decrypted_password('letters.net')
only_letters = store.get_decrypted_password('letters.net').content
self.assertTrue(only_letters.isalpha())

store.generate_password('alphanum.co.uk', digits=True, symbols=False)
alphanum = store.get_decrypted_password('alphanum.co.uk')
alphanum = store.get_decrypted_password('alphanum.co.uk').content
self.assertTrue(alphanum.isalnum())
for char in alphanum:
self.assertTrue(char not in string.punctuation)

store.generate_password('hundred.org', length=100)
length_100 = store.get_decrypted_password('hundred.org')
length_100 = store.get_decrypted_password('hundred.org').content
self.assertEqual(len(length_100), 100)

def test_generate_password_uses_correct_gpg_id(self):
Expand Down Expand Up @@ -313,7 +309,7 @@ def test_generate_in_place(self):
store.insert_password('nope.org', 'pw\nremains intact')
store.generate_password('nope.org', length=3, first_line_only=True)

new_content = store.get_decrypted_password('nope.org')
new_content = store.get_decrypted_password('nope.org').content
new_password, _, remainder = new_content.partition('\n')
self.assertNotEqual(new_password, 'pw')
self.assertEqual(remainder, 'remains intact')

0 comments on commit 5cc58cf

Please sign in to comment.