I've taken up code streaming and I'm concerned that as I do so, I'll leak an API token somewhere.
It was suggested that I use something like "a secrets.toml or secrets.yaml or secrets.json file." But I don't want it all in one file. If I did have a leak, I'd likely lose all my tokens at once.
So I considered a secrets directory, where each file holds each API's token. But what if I accidentally cat the file to the screen?
So I wanted to encrypt the tokens. Because I'm a bit obsessive about these kinds of things. I wouldn't call this encryption, but it is obfuscation, and I think it improves my op-sec quite a bit and it makes me feel a bit better about not leaking api tokens.
I'm using Python, and specifically the following functions and the Path object from the standard library. randbits
will give me as close to cryptographically strong random 0s and 1s as one can get from the Python standard library. getpass
will hide the token if I happen to paste it there. And Path
objects are amazing for dealing with file paths.
from sys import argv
from secrets import randbits
from pathlib import Path
from getpass import getpass
from tempfile import mkdtemp
_USAGE = """usage:
python -m py.token "api name"
python -m py.token --test
"""
def main():
if len(argv) == 2:
if argv[1] == '--test':
test()
else:
try:
print(Manager().get_token(argv[1]))
except:
print(_USAGE)
raise
else:
print(_USAGE)
As the main()
foreshadows, to manage it all I have a Manager
class that can save the tokens and update them, update all of them with a new mask, and for regular usage, to get api tokens:
class Manager:
def __init__(self, root=Path.home()):
self.passtoken_dir = root / 'pass'
self.secrets_dir = self.passtoken_dir / 'secrets'
self.ptfile = self.passtoken_dir / 'token'
self.create_dirs_and_mask()
def create_dirs_and_mask(self):
self.passtoken_dir.mkdir(exist_ok=True)
self.secrets_dir.mkdir(exist_ok=True)
if not self.ptfile.is_file():
self.ptfile.write_bytes(new_passtoken())
else:
# just realized this print() won't work for command line usage.
print('we have a mask file already.')
def save_token(self, api_name='', token=b''):
if not api_name:
api_name = input('api name: ')
if not token:
token = bytes(getpass('input token (hidden): '), 'utf8')
file = self.secrets_dir / api_name
file.write_bytes(mask(token, self.ptfile.read_bytes()))
def update_mask_and_masked_tokens(self):
old_pt = self.ptfile.read_bytes()
new_pt = new_passtoken()
for file in self.secrets_dir.iterdir():
token = unmask(file.read_bytes(), old_pt)
file.write_bytes(mask(token, new_pt))
self.ptfile.write_bytes(new_pt)
def get_token(self, api_name) -> bytes:
return unmask((self.secrets_dir/api_name).read_bytes(),
self.ptfile.read_bytes())
I like having the create_dirs_and_mask
method separate for the possibility of overriding for test purposes, but I didn't go in that direction for testing.
The above Manager object relies heavily on these utility functions to obfuscate and convert from bytes to ints and back.
def new_passtoken() -> bytes:
return int_to_bytes(randbits(8*256))
def bytes_to_int(token) -> int:
return int.from_bytes(token, 'big')
def int_to_bytes(integer) -> bytes:
# wish we didn't have to implement ourselves...
result = []
while integer:
result.append(integer & 255)
integer >>= 8
return bytes(reversed(result))
def mask(token: bytes, passtoken: bytes) -> bytes:
return int_to_bytes(bytes_to_int(token)
^ bytes_to_int(passtoken))
def unmask(masked_token: bytes, passtoken: bytes) -> bytes:
return int_to_bytes(bytes_to_int(masked_token)
^ bytes_to_int(passtoken))
I first implemented mask and unmask with multiply and integer division instead of xor, but it seemed to me that if a leak happened there would be a greater chance of inferring the components of the calculation.
This is all one module, and I haven't yet put the tests in a test module. Tests do pass:
def test(): # TODO put tests in a test package
"""test that functions round-trip
and manager can save, get, update, and still get.
"""
from shutil import rmtree
token = b'abc123}|{'
assert token == int_to_bytes(bytes_to_int(token))
passtoken = b'passtoken'
assert token == unmask(mask(token, passtoken), passtoken)
root = Path(mkdtemp())
try:
manager = Manager(root=root)
api = 'any api'
manager.save_token(api, token)
assert manager.get_token(api) == token
manager.update_mask_and_masked_tokens()
assert manager.get_token(api) == token
globals().update(locals())
finally:
rmtree(root)
For the command line usage, we call the main when we're the entry point for the program:
if __name__ == '__main__':
main()
Please review! I'm especially looking for suggestions that upgrade my attempts at "encryption" without putting a reliance on a third-party library, unless it is quite stable, seasoned, and expected to continue to be in service for the foreseeable future.