Skip to content

Commit 19920a1

Browse files
committed
Major refactor for both Python 2.7 and 3.X compatibility.
1 parent 50c9afa commit 19920a1

File tree

3 files changed

+202
-100
lines changed

3 files changed

+202
-100
lines changed

blob.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2015, Nashwan Azhari.
2+
# Licensed under the GPLv2, see LICENSE file for details.
3+
4+
"""
5+
Basic Blob structure used for data manipulation.
6+
"""
7+
8+
from ctypes import c_char
9+
from ctypes import create_string_buffer
10+
from ctypes import POINTER
11+
from ctypes import Structure
12+
from ctypes import cdll
13+
from ctypes import windll
14+
from ctypes.wintypes import DWORD
15+
16+
memcpy = cdll.msvcrt.memcpy
17+
localfree = windll.kernel32.LocalFree
18+
19+
20+
class Blob(Structure):
21+
"""Basic Structure used for data manipulation.
22+
It is structurally identical to the native CRYPT_INTEGER_BLOB structure.
23+
24+
Composed of a field holding the length of the data and a
25+
pointer to the start of it.
26+
"""
27+
_fields_ = [("length", DWORD), ("data", POINTER(c_char))]
28+
29+
def get_data(self):
30+
"""Fetches the data from the Blob."""
31+
fetched = create_string_buffer(self.length)
32+
33+
memcpy(fetched, self.data, self.length)
34+
35+
return fetched.raw
36+
37+
def free_blob(self):
38+
"""Frees the memory allocated for the Blob's data."""
39+
localfree(self.data)

securestring.py

+114-67
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,124 @@
1-
from ctypes import *
2-
from ctypes.wintypes import DWORD
1+
# Copyright 2015, Nashwan Azhari.
2+
# Licensed under the GPLv2, see LICENSE file for details.
33

4-
protectdata = windll.crypt32.CryptProtectData
5-
unprotectdata = windll.crypt32.CryptUnprotectData
6-
localfree = windll.kernel32.LocalFree
7-
copy = cdll.msvcrt.memcpy
4+
"""
5+
A pure Python implementation of the functionality of the ConvertTo-SecureString
6+
and ConvertFrom-SecureString PoweShell commandlets.
87
9-
# the basic blob structure we will be using for calling
10-
# the above System functions
11-
class blob(Structure):
12-
_fields_ = [("length", DWORD), ("data", POINTER(c_char))]
8+
Usage example:
9+
from securestring import encrypt, decrypt
1310
14-
# this function will fetch all the gata from a given blob
15-
def getblobdata(b):
16-
length = int(b.length)
17-
18-
fetched = c_buffer(length)
19-
copy(fetched, b.data, length)
11+
if __name__ == "__main__":
12+
str = "My horse is amazing"
2013
21-
freeblobdata(b)
22-
return fetched.raw
14+
# encryption:
15+
try:
16+
enc = encrypt(str)
17+
print("The encryption of %s is: %s" % (str, enc))
18+
except Exception as e:
19+
print(e)
2320
24-
# this function will free the memory of the data field from a given blob
25-
def freeblobdata(b):
26-
localfree(b.data)
21+
# decryption:
22+
try:
23+
dec = decrypt(enc)
24+
print("The decryption of the above is: %s" % dec)
25+
except Exception as e:
26+
print(e)
27+
28+
# checking of operation symmetry:
29+
print("Encryption and decryption are symmetrical: %r", dec == str)
30+
31+
# decrypting powershell input:
32+
psenc = "<your output of ConvertFrom-SecureString>"
33+
try:
34+
dec = decrypt(psenc)
35+
print("Decryption from ConvertFrom-SecureString's input: %s" % dec)
36+
except Exception as e:
37+
print(e)
38+
39+
"""
40+
41+
from codecs import encode
42+
from codecs import decode
43+
44+
from blob import Blob
45+
46+
from ctypes import byref
47+
from ctypes import create_string_buffer
48+
from ctypes import windll
49+
50+
protect_data = windll.crypt32.CryptProtectData
51+
unprotect_data = windll.crypt32.CryptUnprotectData
2752

2853

29-
# this function will encrypt a given string in accordance with the
30-
# ConvertFrom-SecureString commandlet and return the hex representation
3154
def encrypt(input):
32-
# for some odd reason the cmdlet's calls encrypt the data with interwoven
33-
# nulls, for which we will account as follows:
34-
nulled = ""
35-
for char in input:
36-
nulled = nulled + char + "\x00"
37-
38-
data = c_buffer(nulled, len(nulled))
39-
40-
inputBlob = blob(len(nulled), data)
41-
entropyBlob = blob()
42-
outputBlob = blob()
43-
flag = 0x01
44-
45-
res = protectdata(byref(inputBlob), u"", byref(entropyBlob), None,
46-
None, flag, byref(outputBlob))
47-
if res == 0:
48-
freeblobdata(outputBlob)
49-
raise Exception("Failed to encrypt " + input)
50-
else:
51-
return getblobdata(outputBlob).encode("hex")
52-
53-
# this function will decrypt the output of ConvertFrom-SecureString
54-
# and return the original string which was encrypted
55+
"""Encrypts the given string following the same syscalls as done by
56+
ConvertFrom-SecureString.
57+
58+
Arguments:
59+
input -- an input string.
60+
61+
Returns:
62+
output -- string containing the output of the encryption in hexadecimal.
63+
"""
64+
# CryptProtectData takes UTF-16; so we must convert the data here:
65+
encoded = input.encode("utf-16")
66+
data = create_string_buffer(encoded, len(encoded))
67+
68+
# create our various Blobs:
69+
input_blob = Blob(len(encoded), data)
70+
output_blob = Blob()
71+
flag = 0x01
72+
73+
# call CryptProtectData:
74+
res = protect_data(byref(input_blob), u"", byref(Blob()), None,
75+
None, flag, byref(output_blob))
76+
input_blob.free_blob()
77+
78+
# check return code:
79+
if res == 0:
80+
output_blob.free_blob()
81+
raise Exception("Failed to encrypt: %s" % input)
82+
else:
83+
raw = output_blob.get_data()
84+
output_blob.free_blob()
85+
86+
# encode the resulting bytes into hexadecimal before returning:
87+
hex = encode(raw, "hex")
88+
return decode(hex, "utf-8").upper()
89+
90+
5591
def decrypt(input):
56-
rawinput = input.decode("hex")
57-
data = c_buffer(rawinput, len(rawinput))
58-
59-
inputBlob = blob(len(rawinput), data)
60-
entropyBlob = blob()
61-
outputBlob = blob()
62-
dwflags = 0x01
63-
64-
res = unprotectdata(byref(inputBlob), u"", byref(entropyBlob), None,
65-
None, dwflags, byref(outputBlob))
66-
if res == 0:
67-
freeblobdata(outputBlob)
68-
raise Exception("Failed to decrypt " + input)
69-
else:
70-
clean = ""
71-
# as mentioned, the commandlets work with data with interwoven nulls,
72-
# for which we must account for by removing them at the end:
73-
for char in getblobdata(outputBlob):
74-
if char != "\x00":
75-
clean = clean + char
76-
return clean
92+
"""Decrypts the given hexadecimally-encoded string in conformity
93+
with CryptUnprotectData.
94+
95+
Arguments:
96+
input -- the encrypted input string in hexadecimal format.
97+
98+
Returns:
99+
output -- string containing the output of decryption.
100+
"""
101+
# de-hex the input:
102+
rawinput = decode(input, "hex")
103+
data = create_string_buffer(rawinput, len(rawinput))
104+
105+
# create out various Blobs:
106+
input_blob = Blob(len(rawinput), data)
107+
output_blob = Blob()
108+
dwflags = 0x01
109+
110+
# call CryptUnprotectData:
111+
res = unprotect_data(byref(input_blob), u"", byref(Blob()), None,
112+
None, dwflags, byref(output_blob))
113+
input_blob.free_blob()
114+
115+
# check return code:
116+
if res == 0:
117+
output_blob.free_blob()
118+
raise Exception("Failed to decrypt: %s" + input)
119+
else:
120+
raw = output_blob.get_data()
121+
output_blob.free_blob()
77122

123+
# decode the resulting data from UTF-16:
124+
return decode(raw, "utf-16")

test_securestring.py

+49-33
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,62 @@
1+
# Copyright 2015, Nashwan Azhari.
2+
# Licensed under the GPLv2, see LICENSE file for details.
3+
4+
from codecs import decode
15
from subprocess import check_output
6+
27
from securestring import encrypt, decrypt
38

49
testinputs = [
5-
"Simple",
6-
"A longer string",
7-
"A!string%with(a4239lot#of$&*special@characters{[]})",
8-
"Quite a very much longer string meant to push the envelope",
9-
"fsdafsgdfgdfgdfgdfgsdfgdgdfgdmmghnh kv dfv dj fkvjjenrwenvfvvslfvnsljfvnlsfvlnsfjlvnssdwoewivdsvmxxvsdvsdv",
10+
"Simple",
11+
"A longer string",
12+
"A!string%with(a4239lot#of$&*special@characters{[]})",
13+
"Quite a very much longer string meant to push the envelope",
14+
"fsdafsgdfgdfgdfgdfgsdfgdgdfgdmmghnh kv dfv dj fkvjjenrwenvfvvslfvnsljfvnlsfvlnsfjlvnssdwoewivdsvmxxvsdvsdv",
1015
]
1116

12-
# tests whether encryption and decryption are symmetrical operations
17+
1318
def test_encryption_decryption_symmetry():
14-
for input in testinputs:
15-
try:
16-
assert decrypt(encrypt(input)) == input
17-
except:
18-
assert False
19+
"""Tests whether encryption and decryption is symmetrical."""
20+
for input in testinputs:
21+
try:
22+
assert decrypt(encrypt(input)) == input
23+
except Exception as e:
24+
print(e)
25+
assert False
1926

2027

2128
def runpscommand(command):
22-
try:
23-
return check_output(["powershell.exe", "-NoProfile",
24-
"-NonInteractive", command]).replace("\r\n", "")
25-
except:
26-
raise Exception("System error has occured.")
29+
"""Helper function which runs the given command with PowerShell
30+
and returns the output.
31+
"""
32+
try:
33+
# NOTE: trailing newline characters must be removed here:
34+
return check_output(["powershell.exe", "-NoProfile",
35+
"-NonInteractive", command]).replace(b"\r\n", b"")
36+
except:
37+
raise Exception("System error has occured.")
38+
2739

28-
# tests whether decrypt() can handle input directly from ConvertFrom-SecureString
2940
def test_decrypt_from_CFSS():
30-
for input in testinputs:
31-
try:
32-
psenc = runpscommand("ConvertTo-SecureString \"%s\" -AsPlainText -Force | ConvertFrom-SecureString" % input)
33-
assert decrypt(psenc) == input
34-
except:
35-
assert False
36-
37-
# tests whether the output of encrypt() is compatible with ConvertTo-SecureString
38-
def test_convert_encrypted_to_securestring():
39-
for input in testinputs:
40-
try:
41-
enc = encrypt(input)
42-
psres = runpscommand("\"%s\" | ConvertTo-SecureString" % enc)
43-
assert psres == "System.Security.SecureString"
44-
except:
45-
assert False
41+
"""Tests whether decrypt() is able to decrypt the
42+
output of ConvertFrom-SecureString."""
43+
for input in testinputs:
44+
try:
45+
psenc = runpscommand("ConvertTo-SecureString \"%s\" -AsPlainText -Force | ConvertFrom-SecureString" % input)
46+
assert decrypt(psenc) == input
47+
except Exception as e:
48+
print(e)
49+
assert False
50+
4651

52+
def test_convert_encrypted_to_securestring():
53+
"""Tests whether the output of encrypt() is compatible with
54+
PowerShell's SecureStrings by feeding its output to ConvertTo-SecureString."""
55+
for input in testinputs:
56+
try:
57+
enc = encrypt(input)
58+
psres = runpscommand("\"%s\" | ConvertTo-SecureString" % enc)
59+
assert decode(psres, "utf-8") == "System.Security.SecureString"
60+
except Exception as e:
61+
print(e)
62+
assert False

0 commit comments

Comments
 (0)