Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

# Obscure Information 

# 

# Defines classes used to conceal or encrypt information found in the accounts 

# file. 

 

# License {{{1 

# Copyright (C) 2016 Kenneth S. Kundert 

# 

# This program 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. 

# 

# This program 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 

# this program. If not, see http://www.gnu.org/licenses/. 

 

 

# Imports {{{1 

from .charsets import DIGITS, DISTINGUISHABLE 

from .config import get_setting, override_setting 

from .dictionary import DICTIONARY 

from .gpg import GnuPG 

from .utilities import error_source 

from inform import ( 

debug, Error, log, indent, is_str, output, terminate, warn, full_stop 

) 

from binascii import a2b_base64, b2a_base64, Error as BinasciiError 

from textwrap import dedent 

import re 

 

# Utilities {{{1 

def chunk(string, length): 

return ( 

string[0+i:length+i] for i in range(0, len(string), length) 

) 

 

def decorate_concealed(name, encoded): 

return '%s(%s)' % ( 

name, 

'\n "' + '"\n "'.join(chunk(encoded, 60)) + '"\n' 

) 

 

def group(pattern): 

return '(?:%s)' % pattern 

 

STRING1 = group('"[^"]+"') 

STRING2 = group("'[^']+'") 

STRING3 = group("'''.+'''") 

STRING4 = group('""".+"""') 

SMPL_STRING = group('{s1}|{s2}'.format(s1=STRING1, s2=STRING2)) 

ML_STRING = group('{s3}|{s4}'.format(s3=STRING3, s4=STRING4)) 

ID = r"\w+" 

DECORATED_LIST = re.compile( 

# Matches: 

# Scrypt( 

# "..." 

# "..." 

# ) 

r"\s*({id})\s*\(\s*((?:{s}\s*)*)\s*\)\s*".format(id=ID, s=SMPL_STRING) 

) 

DECORATED_TEXT = re.compile( 

# Matches: 

# GPG(""" 

# ... 

# ... 

# """) 

r"\s*({id})\s*\(\s*({s})\s*\)\s*".format(id=ID, s=ML_STRING), 

re.DOTALL, 

) 

 

# Obscure {{{1 

class Obscure(object): 

# obscurers() {{{2 

@classmethod 

def obscurers(cls): 

for sub in cls.__subclasses__(): 

if hasattr(sub, 'conceal') and hasattr(sub, 'reveal'): 

yield sub 

for each in sub.obscurers(): 

if hasattr(each, 'conceal') and hasattr(each, 'reveal'): 

yield each 

 

# get_name() {{{2 

@classmethod 

def get_name(cls): 

try: 

return cls.NAME.lower() 

except AttributeError: 

# consider converting lower to upper case transitions in __name__ to 

# dashes. 

return cls.__name__.lower() 

 

# hide() {{{2 

@classmethod 

def hide(cls, text, encoding=None, decorate=False, symmetric=False): 

encoding = encoding.lower() if encoding else 'base64' 

for obscurer in cls.obscurers(): 

if encoding == obscurer.get_name(): 

return obscurer.conceal(text, decorate, symmetric=symmetric) 

raise Error('not found.', culprit=encoding) 

 

# show() {{{2 

@classmethod 

def show(cls, text): 

match = DECORATED_LIST.match(text) 

if match: 

name = match.group(1) 

value = ''.join([s.strip('"' "'") for s in match.group(2).split()]) 

 

for obscurer in cls.obscurers(): 

if name == obscurer.__name__: 

return obscurer.reveal(value) 

raise Error('not found.', culprit=name) 

 

match = DECORATED_TEXT.match(text) 

if match: 

name = match.group(1) 

value = match.group(2) 

 

for obscurer in cls.obscurers(): 

if name == obscurer.__name__: 

return obscurer.reveal(value.strip('"' "'")) 

raise Error('not found.', culprit=name) 

 

return Hidden.reveal(text) 

 

# encodings() {{{2 

@classmethod 

def encodings(cls): 

for c in cls.obscurers(): 

yield c.get_name(), dedent(getattr(c, 'DESC', '')).strip() 

 

# default encoding() {{{2 

@classmethod 

def default_encoding(cls): 

return Hidden.NAME 

 

# __repr__() {{{2 

def __repr__(self): 

return "Hidden('%s')" % (Obscure.hide(self.plaintext, 'base64')) 

 

# Hidden {{{1 

class Hidden(Obscure): 

# This decodes a string that is encoded in base64 to hide it from a casual 

# observer. But it is not encrypted. The original value can be trivially 

# recovered from the encoded version. 

NAME = 'base64' 

DESC = ''' 

This encoding obscures but does not encrypt the text. It can 

protect text from observers that get a quick glance of the 

encoded text, but if they are able to capture it they can easily 

decode it. 

''' 

def __init__(self, ciphertext, secure=True, encoding='utf8'): 

self.ciphertext = ciphertext 

try: 

self.plaintext = a2b_base64(ciphertext).decode(encoding) 

self.secure = secure 

except BinasciiError as err: 

raise Error( 

'invalid value specified to Hidden(): %s.' % str(err), 

culprit=error_source() 

) 

 

def generate(self, field_name, field_key, account): 

# we don't need to do anything, but having this method marks this value 

# as being confidential 

pass 

 

def is_secure(self): 

return self.secure 

 

def __str__(self): 

return self.plaintext 

 

@staticmethod 

def conceal(plaintext, decorate=False, encoding=None, symmetric=False): 

encoding = encoding if encoding else get_setting('encoding') 

plaintext = str(plaintext).encode(encoding) 

encoded = b2a_base64(plaintext).rstrip().decode('ascii') 

if decorate: 

return decorate_concealed('Hidden', encoded) 

else: 

return encoded 

 

@staticmethod 

def reveal(value, encoding=None): 

encoding = encoding if encoding else get_setting('encoding') 

try: 

value = a2b_base64(value.encode('ascii')) 

return value.decode(encoding) 

except BinasciiError as err: 

raise Error('Unable to decode base64 string: %s.' % str(err)) 

except UnicodeDecodeError: 

raise Error('Unable to decode base64 string.') 

 

# GPG {{{1 

class GPG(Obscure, GnuPG): 

DESC = ''' 

This encoding fully encrypts/decrypts the text with GPG key. 

By default your GPG key is used, but you can specify symmetric 

encryption, in which case a passphrase is used. 

''' 

# This does a full GPG decryption. 

# To generate an entry for the GPG argument, you can use ... 

# gpg -a -c filename 

# It will create filename.asc. Copy the contents of that file into the 

# argument. 

# This uses symmetric encryption to add an additional layer of protection. 

# Generally one would use their private key to protect the gpg file, and 

# then use a symmetric key, or perhaps a separate private key, to protect an 

# individual piece of data, like a master password. 

def __init__(self, ciphertext, secure=True, encoding='utf8'): 

self.ciphertext = ciphertext 

 

def generate(self, field_name, field_key, account): 

# must do this here in generate rather than in constructor to avoid 

# decrypting this, and perhaps asking for a passcode, every time 

# Advendesora is run. 

plaintext = self.gpg.decrypt(dedent(self.ciphertext)) 

if not plaintext.ok: 

msg = 'unable to decrypt argument to GPG()' 

try: 

msg = '%s: %s' % (msg, plaintext.stderr) 

except AttributeError: 

msg += '.' 

raise Error(msg, culprit=error_source()) 

self.plaintext = plaintext 

 

def __str__(self): 

return str(self.plaintext) 

 

@classmethod 

def conceal(cls, plaintext, decorate=False, encoding=None, symmetric=False): 

encoding = encoding if encoding else get_setting('encoding') 

plaintext = str(plaintext).encode(encoding) 

gpg_ids = get_setting('gpg_ids', []) 

if is_str(gpg_ids): 

gpg_ids = gpg_ids.split() 

 

encrypted = cls.gpg.encrypt( 

plaintext, gpg_ids, armor=True, symmetric=bool(symmetric) 

) 

if not encrypted.ok: 

msg = ' '.join(cull([ 

'unable to encrypt.', 

getattr(encrypted, 'stderr', None) 

])) 

raise Error(msg) 

ciphertext = str(encrypted) 

if decorate: 

return 'GPG("""\n%s""")' % indent(ciphertext) 

else: 

return ciphertext 

 

@classmethod 

def reveal(cls, ciphertext, encoding=None): 

decrypted = cls.gpg.decrypt(dedent(ciphertext)) 

if not decrypted.ok: 

msg = 'unable to decrypt argument to GPG()' 

try: 

msg = '%s: %s' % (msg, decrypted.stderr) 

except AttributeError: 

pass 

raise Error(full_stop(msg)) 

plaintext = str(decrypted) 

return plaintext 

 

# Scrypt {{{1 

try: 

import scrypt 

class Scrypt(Obscure): 

# This encrypts/decrypts a string with scrypt. The user's key is used as the 

# passcode for this symmetric encryption. 

DESC = ''' 

This encoding fully encrypts the text with your user key. Only 

you can decrypt it, secrets encoded with scrypt cannot be 

shared. 

''' 

def __init__(self, ciphertext, secure=True, encoding='utf8'): 

self.ciphertext = ciphertext 

self.encoding = encoding 

 

def generate(self, field_name, field_key, account): 

encrypted = a2b_base64(self.ciphertext.encode(self.encoding)) 

self.plaintext = scrypt.decrypt(encrypted, get_setting('user_key')) 

 

def is_secure(self): 

return False 

 

def __str__(self): 

return str(self.plaintext).strip() 

 

@staticmethod 

def conceal(plaintext, decorate=False, encoding=None, symmetric=False): 

encoding = encoding if encoding else get_setting('encoding') 

plaintext = str(plaintext).encode(encoding) 

encrypted = scrypt.encrypt( 

plaintext, get_setting('user_key'), maxtime=0.25 

) 

encoded = b2a_base64(encrypted).rstrip().decode('ascii') 

if decorate: 

return decorate_concealed('Scrypt', encoded) 

else: 

return encoded 

 

@staticmethod 

def reveal(ciphertext, encoding=None): 

encrypted = a2b_base64(ciphertext) 

return scrypt.decrypt(encrypted, get_setting('user_key')) 

 

except ImportError: 

pass