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

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

576

577

578

# Account 

# API for an account 

 

# 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 .browsers import StandardBrowser 

from .collection import Collection 

from .config import get_setting 

from .obscure import Obscure 

from .preferences import TOOL_FIELDS 

from .recognize import Recognizer 

from .secrets import Secret 

from inform import ( 

Color, conjoin, cull, Error, is_collection, is_str, log, output, warn, indent 

) 

from textwrap import dedent 

try: 

from urllib.parse import urlparse 

except ImportError: 

from urlparse import urlparse 

import re 

import sys 

 

 

# Globals {{{1 

VECTOR_PATTERN = re.compile(r'\A(\w+)\[(\w+)\]\Z') 

LabelColor = Color( 

color=get_setting('label_color'), 

scheme=get_setting('color_scheme'), 

enable=Color.isTTY() 

) 

 

# AccountValue class {{{1 

class AccountValue: 

"""An Account Value 

 

Contains three attributes: 

value: the actual value 

is_secret: whether the value is secret or contains a secret 

label: a descriptive name for the value if the value of a simple field is requested 

""" 

def __init__(self, value, is_secret, label=None): 

self.value = value 

self.is_secret = is_secret 

self.label = label 

 

def __str__(self): 

return str(self.value) 

 

def render(self, sep=': '): 

if self.label is not None: 

return self.label + sep + str(self.value) 

return str(self.value) 

 

def __iter__(self): 

for each in [self.value, self.is_secret, self.label]: 

yield each 

 

# Account class {{{1 

class Account(object): 

__NO_MASTER = True 

# prevents master password from being added to this base class 

 

# all_accounts() {{{2 

@classmethod 

def all_accounts(cls): 

for sub in cls.__subclasses__(): 

yield sub 

for each in sub.all_accounts(): 

yield each 

 

# fields() {{{2 

@classmethod 

def fields(cls): 

for key, value in cls.__dict__.items(): 

if not key.startswith('_'): 

yield key, value 

 

# get_name() {{{2 

@classmethod 

def get_name(cls): 

try: 

return cls.NAME 

except AttributeError: 

# consider converting lower to upper case transitions in __name__ to 

# dashes. 

return cls.__name__.lower() 

 

# get_seed() {{{2 

@classmethod 

def get_seed(cls): 

try: 

return cls.seed 

except AttributeError: 

return cls.get_name() 

 

# override_master() {{{2 

@classmethod 

def request_seed(cls): 

return getattr(cls, '_interactive_seed', False) 

 

 

# add_fileinfo() {{{2 

@classmethod 

def add_fileinfo(cls, master, fileinfo): 

if master and not hasattr(cls, '_%s__NO_MASTER' % cls.__name__): 

if not hasattr(cls, 'master'): 

cls.master = master 

cls._master_source = 'file' 

else: 

cls._master_source = 'account' 

cls._file_info = fileinfo 

 

# matches_exactly() {{{2 

@classmethod 

def matches_exactly(cls, account): 

if account == cls.get_name(): 

return True 

try: 

if account in Collection(cls.aliases): 

return True 

except AttributeError: 

pass 

return False 

 

# id_contains() {{{2 

@classmethod 

def id_contains(cls, target): 

target = target.lower() 

if target in cls.get_name().lower(): 

return True 

try: 

for alias in Collection(cls.aliases): 

if target in alias.lower(): 

return True 

except AttributeError: 

pass 

return False 

 

# account_contains() {{{2 

@classmethod 

def account_contains(cls, target): 

if cls.id_contains(target): 

return True 

target = target.lower() 

for key, value in cls.fields(): 

if key in TOOL_FIELDS: 

continue 

try: 

if is_collection(value): 

for k, v in Collection(value).items(): 

if target in v.lower(): 

return True 

elif target in value.lower(): 

return True 

except AttributeError: 

# is not a string, and so  

pass 

return False 

 

# recognize() {{{2 

@classmethod 

def recognize(cls, data, verbose): 

# try the specified recognizers 

discovery = getattr(cls, 'discovery', ()) 

for recognizer in Collection(discovery): 

if isinstance(recognizer, Recognizer): 

script = recognizer.match(data, cls, verbose) 

name = getattr(recognizer, 'name', None) 

if script: 

yield name, script 

if discovery: 

return 

 

# If no recognizers specified, just check the urls 

for url in Collection(cls.get_field('urls', default=[])): 

components = urlparse(url) 

protocol = components.scheme 

host = components.netloc 

if host == data.get('host'): 

if ( 

protocol != data.get('protocol') and 

data['protocol'] in get_setting('required_protocols') 

): 

msg = 'url matches, but uses wrong protocol.' 

notify(msg) 

raise Error(msg, culprit=account.get_name()) 

else: 

yield None, True 

return 

 

# initialize() {{{2 

@classmethod 

def initialize(cls, interactive_seed=False): 

cls._interactive_seed = interactive_seed 

log('initializing', cls.get_name()) 

try: 

if cls.master.is_secure(): 

if not cls._file_info.encrypted: 

warn( 

'high value master password not contained in encrypted', 

'account file.', culprit=cls.get_name() 

) 

except AttributeError as err: 

pass 

 

# items() {{{2 

@classmethod 

def items(cls): 

for key in sorted(cls.__dict__): 

if not key.startswith('_'): 

yield key, cls.__dict__[key] 

 

# get_field() {{{2 

@classmethod 

def get_field(cls, name, key=None, default=False): 

"Get field Value given a field name and key" 

value = getattr(cls, name, None) 

if value is None: 

if default is False: 

raise Error( 

'not found.', 

culprit=(cls.get_name(), cls.combine_name(name, key)) 

) 

else: 

return default 

 

if key is None: 

if is_collection(value): 

choices = [] 

for k, v in Collection(value).items(): 

try: 

choices.append(' %s: %s' % (k, v.get_key())) 

except AttributeError: 

choices.append(' %s:' % k) 

raise Error( 

'composite value found, need key. Choose from:', 

*choices, 

sep='\n', 

culprit=name, 

is_collection=True, 

collection = value 

) 

else: 

try: 

if is_collection(value): 

value = value[key] 

else: 

warn('not a composite value, key ignored.', culprit=name) 

key = None 

except (IndexError, KeyError, TypeError): 

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

 

# generate the value if needed 

try: 

value.generate(name, key, cls) 

except AttributeError as err: 

pass 

return value 

 

# is_secret() {{{2 

@classmethod 

def is_secret(cls, name, key=None): 

value = cls.__dict__.get(name) 

if key is None: 

return hasattr(value, 'generate') 

else: 

try: 

return hasattr(value[key], 'generate') 

except (IndexError, KeyError, TypeError): 

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

 

# split_name() {{{2 

@classmethod 

def split_name(cls, name): 

# Account fields can either be scalars or composites (vectors or 

# dictionaries). This function takes a string (name) that the user 

# provides to specify which account value they wish and splits it into a 

# field name and a key. If the field is a scalar, the key will be None. 

# Users request a value using one of the following forms: 

# True: use default name 

# field: scalar value (key=None) 

# index: questions (field->'questions', key=index) 

# field[index] or field/index: for vector value 

# field[key] or field/key: for dictionary value 

 

if name is True or not name: 

name = cls.get_field('default', default=None) 

if not name: 

name = get_setting('default_field') 

 

# convert dashes to underscores 

name = str(name).replace('-', '_') 

 

# If name is an integer, treat it as number of security question. 

try: 

return get_setting('default_vector_field'), int(name) 

except ValueError: 

pass 

 

# Split name if given in the form: name/key 

try: 

name, key = name.split('.') 

try: 

return name, int(key) 

except ValueError: 

return name, key 

except ValueError: 

pass 

 

# Split name if given in the form: name[key] 

match = VECTOR_PATTERN.match(name) 

if match: 

# vector name using 'name[key]' syntax 

name, key = match.groups() 

try: 

return name, int(key) 

except ValueError: 

return name, key 

 

# Must be scalar name 

return name, None 

 

# combine_name() {{{2 

@classmethod 

def combine_name(cls, name, key=None): 

# Inverse of split_name(). 

 

# convert underscores to dashes 

#name = name.replace('_', '-') 

 

if key is None: 

return name 

else: 

return '%s.%s' % (name, key) 

 

# get_value() {{{2 

@classmethod 

def get_value(cls, field=None): 

"""Get Account Value 

 

Return value from the account given a user friendly identifier or 

script. User friendly identifiers include: 

None: value of default attribute 

name: scalar value 

name.key or name[key]: 

member of a dictionary or array 

key is string for dictionary, integer for array 

Scripts are simply strings with embedded attributes. Ex: 

'username: {username}, password: {passcode}' 

Returns a tuple: value, is_secret, label 

""" 

 

# get default if field was not given 

if not field: 

name, key = cls.split_name(field) 

field = '.'.join(cull([name, key])) 

 

not_script = not (is_str(field) and '{' in field) 

 

# treat field as name rather than script if it there are no attributes 

if not_script: 

name, key = cls.split_name(field) 

try: 

value = cls.get_field(name, key) 

except Error as err: 

err.terminate() 

is_secret = cls.is_secret(name, key) 

label = cls.combine_name(name, key) 

try: 

alt_name = value.get_key() 

if alt_name: 

label += ' (%s)' % alt_name 

except AttributeError: 

pass 

if isinstance(value, Secret) or isinstance(value, Obscure): 

value = str(value) 

value = dedent(value).strip() if is_str(value) else value 

return AccountValue(value, is_secret, label) 

 

# run the script 

script = field 

regex = re.compile(r'({[\w. ]+})') 

out = [] 

is_secret = False 

for term in regex.split(script): 

if term and term[0] == '{' and term[-1] == '}': 

# we have found a command 

cmd = term[1:-1].lower() 

if cmd == 'tab': 

out.append('\t') 

elif cmd == 'return': 

out.append('\n') 

elif cmd.startswith('sleep '): 

pass 

else: 

name, key = cls.split_name(cmd) 

try: 

value = cls.get_field(name, key) 

out.append(dedent(str(value)).strip()) 

if cls.is_secret(name, key): 

is_secret = True 

except Error as err: 

err.terminate() 

else: 

out.append(term) 

return AccountValue(''.join(out), is_secret) 

 

 

 

 

 

 

 

value = cls.get_field(*cls.split_name(name)) 

return value 

 

# write_summary() {{{2 

@classmethod 

def write_summary(cls): 

# present all account values that are not explicitly secret to the user 

 

def fmt_field(key, value='', level=0): 

if '\n' in value: 

value = indent(dedent(value), get_setting('indent')).strip('\n') 

sep = '\n' 

elif value: 

sep = ' ' 

else: 

sep = '' 

key = str(key).replace('_', ' ') 

leader = level*get_setting('indent') 

return indent(LabelColor(key + ':') + sep + value, leader) 

 

def reveal(name, key=None): 

return "<reveal with 'avendesora value %s %s'>" % ( 

cls.get_name(), cls.combine_name(name, key) 

) 

 

def extract_collection(name, collection): 

lines = [fmt_field(key)] 

for k, v in Collection(collection).items(): 

if hasattr(v, 'generate'): 

# is a secret, get description if available 

try: 

v = '%s %s' % (v.get_key(), reveal(name, k)) 

except AttributeError: 

v = reveal(name, k) 

lines.append(fmt_field(k, v, level=1)) 

return lines 

 

# preload list with the names associated with this account 

names = [cls.get_name()] 

if hasattr(cls, 'aliases'): 

names += Collection(cls.aliases) 

lines = [fmt_field('names', ', '.join(names))] 

 

for key, value in cls.items(): 

if key in TOOL_FIELDS: 

pass # is an Avendesora field 

elif is_collection(value): 

lines += extract_collection(key, value) 

elif hasattr(value, 'generate'): 

lines.append(fmt_field(key, reveal(key))) 

else: 

lines.append(fmt_field(key, value)) 

output(*lines, sep='\n') 

 

# archive() {{{2 

@classmethod 

def archive(cls): 

# return all account fields along with their values as a dictionary 

 

def extract(value, name, key=None): 

if not is_collection(value): 

if hasattr(value, 'generate'): 

value.generate(name, key, cls) 

#value = 'Hidden(%s)' % Obscure.hide(str(value)) 

return value 

try: 

return {k: extract(v, name, k) for k, v in value.items()} 

except AttributeError: 

# still need to work out how to output the question. 

return [extract(v, name, i) for i, v in enumerate(value)] 

 

return {k: extract(v, k) for k, v in cls.items() if k != 'master'} 

 

# open_browser() {{{2 

@classmethod 

def open_browser(cls, browser_name, key=None): 

if not browser_name: 

browser_name = cls.get_field('browser', default=None) 

browser = StandardBrowser(browser_name) 

 

# get the urls from the urls attribute 

if not key: 

key = getattr(cls, 'default_url', None) 

urls = getattr(cls, 'urls', []) 

if type(urls) != dict: 

if is_str(urls): 

urls = urls.split() 

urls = {None: urls} 

 

# get the urls from the url recognizers 

# currently urls from recognizers dominate over those from attributes 

discovery = getattr(cls, 'discovery', ()) 

for each in Collection(discovery): 

urls.update(each.all_urls()) 

 

# select the urls 

try: 

urls = urls[key] 

except TypeError: 

if key: 

raise Error( 

'keys are not supported with urls on this account.', 

culprit=key 

) 

except KeyError: 

keys = cull(urls.keys()) 

if keys: 

raise Error( 

'unknown key, choose from %s.' % conjoin(keys), 

culprit=key 

) 

else: 

raise Error( 

'keys are not supported with urls on this account.', 

culprit=key 

) 

url = list(Collection(urls))[0] # use the first url specified 

 

# open the url 

browser.run(url) 

 

 

# StealthAccount class {{{1 

class StealthAccount(Account): 

__NO_MASTER = True 

# prevents master password from being added to this base class 

 

@classmethod 

def get_seed(cls): 

# need to handle case where stdin/stdout is not available. 

# perhaps write generic password getter that supports both gui and tui. 

# Then have global option that indicates which should be used. 

# Separate name from seed. Only request seed when generating a password. 

import getpass 

try: 

name = getpass.getpass('account name: ') 

except EOFError: 

output() 

name = '' 

if not name: 

warn('null account name.') 

return name 

 

@classmethod 

def archive(cls): 

# do not archive stealth accounts 

pass