Skip to content

Commit b5e6787

Browse files
authoredOct 18, 2017
Merge pull request #127 from seleniumbase/password-obfuscation
Password Obfuscation
2 parents a264cc1 + e1b29e5 commit b5e6787

File tree

7 files changed

+292
-5
lines changed

7 files changed

+292
-5
lines changed
 

‎seleniumbase/common/ReadMe.md

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## Using methods from the "common" folder.
2+
3+
### Part 1: Decorators - (from [decorators.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/decorators.py))
4+
5+
#### Use these Python decorators with your test methods as needed:
6+
7+
* @retry_on_exception(tries=6, delay=1, backoff=2, max_delay=32)
8+
9+
* @rate_limited(max_per_second)
10+
11+
Example demonstrating a rate-limited printing functionality:
12+
```python
13+
import unittest
14+
from seleniumbase.common import decorators
15+
16+
17+
class MyTestClass(unittest.TestCase):
18+
19+
@decorators.rate_limited(3.5) # The arg is max calls per second
20+
def print_item(self, item):
21+
print(item)
22+
23+
def test_rate_limited_printing(self):
24+
print("\nRunning rate-limited print test:")
25+
for item in range(1, 11):
26+
self.print_item(item)
27+
```
28+
29+
### Part 2: String/Password Obfuscation, Encryption, and Decryption
30+
31+
#### Intro:
32+
33+
Often in your tests, you may need to login to a website to perform testing. This generally means storing passwords in plaintext formats. For security reasons, that may not be an optimal solution. For this reason, encryption/obfuscation tools have been built here to help you mask your passwords in your tests. It's not a bulletproof solution, but it can keep anyone looking over your shoulder during test creation from getting your login passwords if they don't have your encryption key, which is stored in a separate file.
34+
35+
#### Usage:
36+
37+
* First, set your custom encryption/decryption key in your local clone of [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py). (If you modify they key later, you'll need to encrypt all your passwords again.)
38+
39+
* Next, use [obfuscate.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/obfuscate.py) to obfuscate/encrypt passwords into coded strings:
40+
```bash
41+
python obfuscate.py
42+
43+
Enter password to obfuscate: (CTRL-C to exit)
44+
Password: *********
45+
Verify password:
46+
Password: *********
47+
48+
Here is the obfuscated password:
49+
$^*ENCRYPT=RXlYMSJWTz8HSwM=?&#$
50+
```
51+
(You can also use [unobfuscate.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/unobfuscate.py) to encrypt passwords without having them masked while typing them. Or you can use it to decrypt an obfuscated pasword.)
52+
53+
* Finally, in your tests you can now decrypt obfuscated passwords for use in login methods like this:
54+
```python
55+
from seleniumbase.common import encryption
56+
...
57+
password = encryption.decrypt('$^*ENCRYPT=RXlYMSJWTz8HSwM=?&#$')
58+
```
59+
(You'll notice that encrypted strings have a common start token and end token. This is to help tell them apart from non-encrypted strings. You can customize these tokens in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py). The current default setting is `$^*ENCRYPT=` for the start token and `?&#$` for the end token.)

‎seleniumbase/common/encryption.py

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# -\*- coding: utf-8 -\*-
2+
3+
''' This is mainly for string obfuscation. '''
4+
5+
import base64
6+
import codecs
7+
import hashlib
8+
from seleniumbase.config import settings
9+
10+
11+
def str_xor(string, key):
12+
if len(key) < 1:
13+
raise Exception("2nd arg of str_xor() must be a string of length > 0!")
14+
if len(string) > len(key):
15+
difference = len(string) - len(key)
16+
key = key + (
17+
((difference / len(key)) * key) + key)
18+
result = None
19+
try:
20+
result = "".join(
21+
[chr(ord(c1) ^ ord(c2)) for (c1, c2) in zip(string, key)])
22+
except:
23+
string = string.decode('utf-8')
24+
result = "".join(
25+
[chr(ord(c1) ^ ord(c2)) for (c1, c2) in zip(string, key)])
26+
return result
27+
28+
29+
def is_obfuscated(string):
30+
# Based on settings, determines if a string has already been obfuscated.
31+
# Obfuscated strings have a common predefined start token and end token.
32+
start_token = settings.OBFUSCATION_START_TOKEN
33+
end_token = settings.OBFUSCATION_END_TOKEN
34+
return (string.startswith(start_token) and string.endswith(end_token))
35+
36+
37+
def shuffle_string(string):
38+
if len(string) < 2:
39+
return string
40+
return (string[1::2] + string[::2])
41+
42+
43+
def reverse_shuffle_string(string):
44+
if len(string) < 2:
45+
return string
46+
new_string = ""
47+
odd = (len(string) % 2 == 1)
48+
part1 = string[:int(len(string)/2):1]
49+
part2 = string[int(len(string)/2)::1]
50+
for c in range(len(part1)):
51+
new_string += part2[c]
52+
new_string += part1[c]
53+
if odd:
54+
new_string += part2[-1]
55+
return new_string
56+
57+
58+
def blend_strings(string1, string2):
59+
smallest_length = min(len(string1), len(string2))
60+
new_string = ""
61+
for c in range(smallest_length):
62+
new_string += string1[c]
63+
new_string += string2[c]
64+
if len(string1) > len(string2):
65+
new_string += string1[smallest_length:]
66+
elif len(string2) > len(string1):
67+
new_string += string2[smallest_length:]
68+
else:
69+
# Equal length strings
70+
pass
71+
return new_string
72+
73+
74+
def rotate(l, n):
75+
return l[n:] + l[:n]
76+
77+
78+
def ord_string_sum(string):
79+
count = 0
80+
try:
81+
for c in string:
82+
count += ord(c)
83+
except:
84+
string = string.decode('utf-8')
85+
for c in string:
86+
count += ord(c)
87+
return count
88+
89+
90+
def decrypt(string):
91+
# Password/String obfuscation/de-obfuscation
92+
# Used for both encryption and decryption
93+
# If you update the algorithm, you must re-encrypt all encrypted passwords!
94+
encryption_key = settings.ENCRYPTION_KEY
95+
start_token = settings.OBFUSCATION_START_TOKEN
96+
end_token = settings.OBFUSCATION_END_TOKEN
97+
already_encrypted = False
98+
if is_obfuscated(string):
99+
already_encrypted = True
100+
string = string[len(start_token):-len(end_token)]
101+
string = base64.b64decode(codecs.encode(string))
102+
# Obfuscate the key used for string obfuscation
103+
hd1 = hashlib.sha256(str(encryption_key).encode('utf-8')).hexdigest()
104+
hd2 = hashlib.sha256(str(encryption_key[::-1]).encode('utf-8')).hexdigest()
105+
b64_key = base64.b64encode(codecs.encode(encryption_key * 8))
106+
xor_key = "".join([chr(ord(str(c3)) - int(c1, 16) - int(c2, 16)) for (
107+
c1, c2, c3) in zip(hd1, hd2, b64_key.decode("utf-8"))])
108+
xor_key = blend_strings(xor_key, encryption_key)
109+
if len(xor_key) % 7 == 0:
110+
xor_key = xor_key + encryption_key[-1]
111+
xor_key = shuffle_string((xor_key * 8)[::7])
112+
# Use the str_xor method for the main string obfuscation / de-obfuscation
113+
if not already_encrypted:
114+
if len(string) > 0:
115+
rem1 = (ord_string_sum(string)) % 3
116+
rem2 = (ord_string_sum(string)) % 4
117+
rem3 = (ord_string_sum(string)) % 2
118+
rem4 = (len(string) + ord_string_sum(string)) % 2
119+
if len(string) % 2 != 0:
120+
if rem3 == 1:
121+
string = (chr(ord(string[-1])-5-rem1) + string +
122+
chr(ord(string[-1])-13-rem1))
123+
else:
124+
string = (chr(ord(string[-1])-11-rem1) + string +
125+
chr(ord(string[-1])-23-rem1))
126+
elif len(string) > 1:
127+
if rem4 == 1:
128+
string = (chr(ord(string[0])-19+rem2) + string +
129+
chr(ord(string[0])-7-rem2))
130+
else:
131+
string = (chr(ord(string[0])-26+rem2) + string +
132+
chr(ord(string[0])-12-rem2))
133+
rem5 = (len(string) + ord_string_sum(string)) % 23
134+
string = rotate(string, rem5)
135+
result = str_xor(shuffle_string(string)[::-1], xor_key)
136+
rem6 = (len(result) + ord_string_sum(result)) % 17
137+
result = rotate(result, rem6)
138+
else:
139+
rem6 = (len(string) + ord_string_sum(string)) % 17
140+
string = rotate(string, -rem6)
141+
result = reverse_shuffle_string(str_xor(string, xor_key)[::-1])
142+
if len(result) > 2:
143+
rem5 = (len(result) + ord_string_sum(result)) % 23
144+
result = rotate(result, -rem5)
145+
result = result[1:-1]
146+
# Finalize encryption of non-encrypted string
147+
if not already_encrypted:
148+
result = base64.b64encode(codecs.encode(result))
149+
result = start_token + result.decode("utf-8") + end_token
150+
return result

‎seleniumbase/common/obfuscate.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Obfuscates a string/password into a string that can be decrypted later on.
3+
4+
Usage:
5+
python obfuscate.py
6+
Then enter the password.
7+
The result is an encrypted password.
8+
"""
9+
10+
from seleniumbase.common import encryption
11+
import getpass
12+
import time
13+
14+
15+
def main():
16+
try:
17+
while(1):
18+
print("\nEnter password to obfuscate: (CTRL-C to exit)")
19+
password = getpass.getpass()
20+
print("Verify password:")
21+
verify_password = getpass.getpass()
22+
if password != verify_password:
23+
print("*** ERROR: Passwords don't match! .. Please try again!")
24+
continue
25+
print("\nHere is the obfuscated password:")
26+
time.sleep(0.07)
27+
print(encryption.decrypt(password))
28+
time.sleep(0.21)
29+
except:
30+
print("\nExiting...\n")
31+
32+
33+
if __name__ == "__main__":
34+
main()

‎seleniumbase/common/unobfuscate.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Unobfuscates an encrypted string/password into a plaintext string/password.
3+
4+
Usage:
5+
python unobfuscate.py
6+
Then enter the encrypted string/password.
7+
The result is a plaintext string/password.
8+
Works the same as obfuscate.py, but doesn't mask the input.
9+
"""
10+
11+
from seleniumbase.common import encryption
12+
import time
13+
14+
15+
def main():
16+
try:
17+
# Python 2 has the raw_input() method. Python 3 does not.
18+
input_method = raw_input # noqa: ignore=F821
19+
except:
20+
input_method = input # Using Python 3
21+
try:
22+
while(1):
23+
code = input_method(
24+
'\nEnter obfuscated/encrypted string: (CTRL-C to exit):\n')
25+
print("\nHere is the unobfuscated string/password:")
26+
time.sleep(0.07)
27+
print(encryption.decrypt(code))
28+
time.sleep(0.21)
29+
except:
30+
print("\nExiting...\n")
31+
32+
33+
if __name__ == "__main__":
34+
main()

‎seleniumbase/config/settings.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494

9595

9696
# #####>>>>>----- RECOMMENDED SETTINGS -----<<<<<#####
97-
# ##### (For database reporting and saving test logs)
97+
# ##### (For database reporting, saving test logs, and password encryption)
9898

9999
# MySQL DB Credentials
100100
# (For saving data from tests)
@@ -113,6 +113,16 @@
113113
S3_SELENIUM_SECRET_KEY = "[S3 SECRET KEY]"
114114

115115

116+
# ENCRYPTION SETTINGS
117+
# (Used for string/password obfuscation)
118+
# (You should reset the Encryption Key for every clone of SeleniumBase)
119+
ENCRYPTION_KEY = "Pg^.l!8UdJ+Y7dMIe&fl*%!p9@ej]/#tL~3E4%6?"
120+
# These tokens are added to the beginning and end of obfuscated passwords.
121+
# Helps identify which strings/passwords have been obfuscated.
122+
OBFUSCATION_START_TOKEN = "$^*ENCRYPT="
123+
OBFUSCATION_END_TOKEN = "?&#$"
124+
125+
116126
# #####>>>>>----- OPTIONAL SETTINGS -----<<<<<#####
117127
# ##### (For reading emails, notifying people via chat apps, etc.)
118128

‎server_setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
setup(
1010
name='seleniumbase',
11-
version='1.4.9',
11+
version='1.4.10',
1212
description='Test Automation Framework - http://seleniumbase.com',
13-
long_description='Automation Framework for Simple & Reliable Web Testing',
13+
long_description='Simple & Reliable Web Automation for Pytest & Nosetests',
1414
platforms='Mac * Windows * Linux * Docker',
1515
url='http://seleniumbase.com',
1616
author='Michael Mintz',

‎setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
setup(
99
name='seleniumbase',
10-
version='1.4.9',
10+
version='1.4.10',
1111
description='Test Automation Framework - http://seleniumbase.com',
12-
long_description='Automation Framework for Simple & Reliable Web Testing',
12+
long_description='Simple & Reliable Web Automation for Pytest & Nosetests',
1313
platforms='Mac * Windows * Linux * Docker',
1414
url='http://seleniumbase.com',
1515
author='Michael Mintz',

0 commit comments

Comments
 (0)