__main__.py 15.7 KB
Newer Older
1
2
#! /usr/bin/env python

3
4
from __future__ import print_function

5
6
7
8
9
10
11
12
import argparse
import json
import os
import sys
import yaml

from . import *

13

14
15
16
17
18
def read_base64_file(filename):
    """Read a base64 file, dropping any CR/LF characters"""
    with open(filename, "rb") as f:
        return f.read().translate(None, "\r\n")

19

20
def build_arg_parser():
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
    parser = argparse.ArgumentParser()
    parser.add_argument("--key", help="Account encryption key", default="")
    commands = parser.add_subparsers()

    create_account = commands.add_parser("create_account", help="Create a new account")
    create_account.add_argument("account_file", help="Local account file")

    def do_create_account(args):
        if os.path.exists(args.account_file):
            sys.stderr.write("Account %r file already exists" % (
                args.account_file,
            ))
            sys.exit(1)
        account = Account()
        account.create()
        with open(args.account_file, "wb") as f:
            f.write(account.pickle(args.key))

    create_account.set_defaults(func=do_create_account)

    keys = commands.add_parser("keys", help="List public keys for an account")
    keys.add_argument("account_file", help="Local account file")
    keys.add_argument("--json", action="store_true", help="Output as JSON")

    def do_keys(args):
        account = Account()
47
        account.unpickle(args.key, read_base64_file(args.account_file))
48
49
50
51
52
53
54
55
56
57
58
59
60
61
        result = {
            "account_keys": account.identity_keys(),
            "one_time_keys": account.one_time_keys(),
        }
        try:
            if args.json:
                json.dump(result, sys.stdout, indent=4)
            else:
                yaml.safe_dump(result, sys.stdout, default_flow_style=False)
        except:
            pass

    keys.set_defaults(func=do_keys)

62
63
    def do_id_key(args):
        account = Account()
64
        account.unpickle(args.key, read_base64_file(args.account_file))
65
66
67
68
69
70
71
72
        print(account.identity_keys()['curve25519'])

    id_key = commands.add_parser("identity_key", help="Get the identity key for an account")
    id_key.add_argument("account_file", help="Local account file")
    id_key.set_defaults(func=do_id_key)

    def do_one_time_key(args):
        account = Account()
73
        account.unpickle(args.key, read_base64_file(args.account_file))
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
        keys = account.one_time_keys()['curve25519'].values()
        key_num = args.key_num
        if key_num < 1 or key_num > len(keys):
            print(
                "Invalid key number %i: %i keys available" %
                   (key_num, len(keys)),
                file=sys.stderr
            )
            sys.exit(1)
        print (keys[key_num-1])

    one_time_key = commands.add_parser("one_time_key",
                                       help="Get a one-time key for the account")
    one_time_key.add_argument("account_file", help="Local account file")
    one_time_key.add_argument("--key-num", "-n", type=int, default=1,
                              help="Index of key to retrieve (default: 1)")
    one_time_key.set_defaults(func=do_one_time_key)


93
94
95
96
97
98
99
    sign = commands.add_parser("sign", help="Sign a message")
    sign.add_argument("account_file", help="Local account file")
    sign.add_argument("message_file", help="Message to sign")
    sign.add_argument("signature_file", help="Signature to output")

    def do_sign(args):
        account = Account()
100
        account.unpickle(args.key, read_base64_file(args.account_file))
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
        with open_in(args.message_file) as f:
             message = f.read()
        signature = account.sign(message)
        with open_out(args.signature_file) as f:
             f.write(signature)

    sign.set_defaults(func=do_sign)


    generate_keys = commands.add_parser("generate_keys", help="Generate one time keys")
    generate_keys.add_argument("account_file", help="Local account file")
    generate_keys.add_argument("count", type=int, help="Number of keys to generate")

    def do_generate_keys(args):
        account = Account()
116
        account.unpickle(args.key, read_base64_file(args.account_file))
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
        account.generate_one_time_keys(args.count)
        with open(args.account_file, "wb") as f:
            f.write(account.pickle(args.key))

    generate_keys.set_defaults(func=do_generate_keys)


    outbound = commands.add_parser("outbound", help="Create an outbound session")
    outbound.add_argument("account_file", help="Local account file")
    outbound.add_argument("session_file", help="Local session file")
    outbound.add_argument("identity_key", help="Remote identity key")
    outbound.add_argument("one_time_key", help="Remote one time key")

    def do_outbound(args):
        if os.path.exists(args.session_file):
            sys.stderr.write("Session %r file already exists" % (
                args.session_file,
            ))
            sys.exit(1)
        account = Account()
137
        account.unpickle(args.key, read_base64_file(args.account_file))
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
        session = Session()
        session.create_outbound(
            account, args.identity_key, args.one_time_key
        )
        with open(args.session_file, "wb") as f:
            f.write(session.pickle(args.key))

    outbound.set_defaults(func=do_outbound)

    def open_in(path):
        if path == "-":
            return sys.stdin
        else:
            return open(path, "rb")

    def open_out(path):
        if path == "-":
            return sys.stdout
        else:
            return open(path, "wb")

    inbound = commands.add_parser("inbound", help="Create an inbound session")
    inbound.add_argument("account_file", help="Local account file")
    inbound.add_argument("session_file", help="Local session file")
    inbound.add_argument("message_file", help="Message", default="-")
    inbound.add_argument("plaintext_file", help="Plaintext", default="-")

    def do_inbound(args):
        if os.path.exists(args.session_file):
            sys.stderr.write("Session %r file already exists" % (
                args.session_file,
            ))
            sys.exit(1)
        account = Account()
172
        account.unpickle(args.key, read_base64_file(args.account_file))
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
        with open_in(args.message_file) as f:
            message_type = f.read(8)
            message = f.read()
        if message_type != "PRE_KEY ":
            sys.stderr.write("Expecting a PRE_KEY message")
            sys.exit(1)
        session = Session()
        session.create_inbound(account, message)
        plaintext = session.decrypt(0, message)
        with open(args.session_file, "wb") as f:
            f.write(session.pickle(args.key))
        with open_out(args.plaintext_file) as f:
            f.write(plaintext)

    inbound.set_defaults(func=do_inbound)

    session_id = commands.add_parser("session_id", help="Session ID")
    session_id.add_argument("session_file", help="Local session file")

    def do_session_id(args):
        session = Session()
194
        session.unpickle(args.key, read_base64_file(args.session_file))
195
196
197
198
199
200
201
202
203
204
205
        sys.stdout.write(session.session_id() + "\n")

    session_id.set_defaults(func=do_session_id)

    encrypt = commands.add_parser("encrypt", help="Encrypt a message")
    encrypt.add_argument("session_file", help="Local session file")
    encrypt.add_argument("plaintext_file", help="Plaintext", default="-")
    encrypt.add_argument("message_file", help="Message", default="-")

    def do_encrypt(args):
        session = Session()
206
        session.unpickle(args.key, read_base64_file(args.session_file))
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
        with open_in(args.plaintext_file) as f:
            plaintext = f.read()
        message_type, message = session.encrypt(plaintext)
        with open(args.session_file, "wb") as f:
            f.write(session.pickle(args.key))
        with open_out(args.message_file) as f:
            f.write(["PRE_KEY ", "MESSAGE "][message_type])
            f.write(message)

    encrypt.set_defaults(func=do_encrypt)

    decrypt = commands.add_parser("decrypt", help="Decrypt a message")
    decrypt.add_argument("session_file", help="Local session file")
    decrypt.add_argument("message_file", help="Message", default="-")
    decrypt.add_argument("plaintext_file", help="Plaintext", default="-")

    def do_decrypt(args):
        session = Session()
225
        session.unpickle(args.key, read_base64_file(args.session_file))
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
        with open_in(args.message_file) as f:
            message_type = f.read(8)
            message = f.read()
        if message_type not in {"PRE_KEY ", "MESSAGE "}:
            sys.stderr.write("Expecting a PRE_KEY or MESSAGE message")
            sys.exit(1)
        message_type = 1 if message_type == "MESSAGE " else 0
        plaintext = session.decrypt(message_type, message)
        with open(args.session_file, "wb") as f:
            f.write(session.pickle(args.key))
        with open_out(args.plaintext_file) as f:
            f.write(plaintext)

    decrypt.set_defaults(func=do_decrypt)

241
242
243
244
    outbound_group = commands.add_parser("outbound_group", help="Create an outbound group session")
    outbound_group.add_argument("session_file", help="Local group session file")
    outbound_group.set_defaults(func=do_outbound_group)

245
246
247
248
249
250
251
    group_credentials = commands.add_parser("group_credentials", help="Export the current outbound group session credentials")
    group_credentials.add_argument("session_file", help="Local outbound group session file")
    group_credentials.add_argument("credentials_file", help="File to write credentials to (default stdout)",
                                   type=argparse.FileType('w'), nargs='?',
                                   default=sys.stdout)
    group_credentials.set_defaults(func=do_group_credentials)

252
    group_encrypt = commands.add_parser("group_encrypt", help="Encrypt a group message")
253
254
255
256
257
258
259
    group_encrypt.add_argument("session_file", help="Local outbound group session file")
    group_encrypt.add_argument("plaintext_file", help="Plaintext file (default stdin)",
                               type=argparse.FileType('rb'), nargs='?',
                               default=sys.stdin)
    group_encrypt.add_argument("message_file", help="Message file (default stdout)",
                               type=argparse.FileType('w'), nargs='?',
                               default=sys.stdout)
260
261
    group_encrypt.set_defaults(func=do_group_encrypt)

262
263
264
265
266
267
268
269
270
271
272
    inbound_group = commands.add_parser(
        "inbound_group",
        help=("Create an inbound group session based on credentials from an "+
              "outbound group session"))
    inbound_group.add_argument("session_file", help="Local inbound group session file")
    inbound_group.add_argument("credentials_file",
                               help="File to read credentials from (default stdin)",
                               type=argparse.FileType('r'), nargs='?',
                               default=sys.stdin)
    inbound_group.set_defaults(func=do_inbound_group)

273
274
275
276
277
278
279
280
281
282
283
284
285
    import_inbound_group = commands.add_parser(
        "import_inbound_group",
        help="Create an inbound group session based an exported inbound group"
    )
    import_inbound_group.add_argument("session_file", help="Local inbound group session file")
    import_inbound_group.add_argument(
        "export_file",
        help="File to read credentials from (default stdin)",
        type=argparse.FileType('r'), nargs='?',
        default=sys.stdin,
    )
    import_inbound_group.set_defaults(func=do_import_inbound_group)

286
287
288
289
290
291
292
293
294
    group_decrypt = commands.add_parser("group_decrypt", help="Decrypt a group message")
    group_decrypt.add_argument("session_file", help="Local inbound group session file")
    group_decrypt.add_argument("message_file", help="Message file (default stdin)",
                               type=argparse.FileType('r'), nargs='?',
                               default=sys.stdin)
    group_decrypt.add_argument("plaintext_file", help="Plaintext file (default stdout)",
                               type=argparse.FileType('wb'), nargs='?',
                               default=sys.stdout)
    group_decrypt.set_defaults(func=do_group_decrypt)
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314

    export_inbound_group = commands.add_parser(
        "export_inbound_group",
        help="Export the keys for an inbound group session",
    )
    export_inbound_group.add_argument(
        "session_file", help="Local inbound group session file",
    )
    export_inbound_group.add_argument(
        "export_file", help="File to export to (default stdout)",
        type=argparse.FileType('w'), nargs='?',
        default=sys.stdout,
    )
    export_inbound_group.add_argument(
        "--message_index",
        help="Index to export session at. Defaults to the earliest known index",
        type=int,
    )
    export_inbound_group.set_defaults(func=do_export_inbound_group)

315
316
    ed25519_verify = commands.add_parser("ed25519_verify", help="Verify an ed25519 signature")
    ed25519_verify.set_defaults(func=do_verify_ed25519_signature)
317
318
319
320
321
322
323
324
325
326
327
328
329
330
    return parser

def do_outbound_group(args):
    if os.path.exists(args.session_file):
        sys.stderr.write("Session %r file already exists" % (
            args.session_file,
        ))
        sys.exit(1)
    session = OutboundGroupSession()
    with open(args.session_file, "wb") as f:
            f.write(session.pickle(args.key))

def do_group_encrypt(args):
    session = OutboundGroupSession()
331
    session.unpickle(args.key, read_base64_file(args.session_file))
332
333
334
335
336
337
    plaintext = args.plaintext_file.read()
    message = session.encrypt(plaintext)
    with open(args.session_file, "wb") as f:
        f.write(session.pickle(args.key))
    args.message_file.write(message)

338
339
def do_group_credentials(args):
    session = OutboundGroupSession()
340
    session.unpickle(args.key, read_base64_file(args.session_file))
341
342
343
344
345
346
347
348
349
350
351
352
353
    result = {
        'message_index': session.message_index(),
        'session_key': session.session_key(),
    }
    json.dump(result, args.credentials_file, indent=4)

def do_inbound_group(args):
    if os.path.exists(args.session_file):
        sys.stderr.write("Session %r file already exists\n" % (
            args.session_file,
        ))
        sys.exit(1)
    credentials = json.load(args.credentials_file)
354
    for k in ('session_key', ):
355
356
357
358
359
        if not k in credentials:
            sys.stderr.write("Credentials file is missing %s\n" % k)
            sys.exit(1);

    session = InboundGroupSession()
360
    session.init(credentials['session_key'])
361
362
363
    with open(args.session_file, "wb") as f:
        f.write(session.pickle(args.key))

364
365
366
367
368
369
370
371
372
373
374
375
376
def do_import_inbound_group(args):
    if os.path.exists(args.session_file):
        sys.stderr.write("Session %r file already exists\n" % (
            args.session_file,
        ))
        sys.exit(1)
    data = args.export_file.read().translate(None, "\r\n")

    session = InboundGroupSession()
    session.import_session(data)
    with open(args.session_file, "wb") as f:
        f.write(session.pickle(args.key))

377
378
def do_group_decrypt(args):
    session = InboundGroupSession()
379
    session.unpickle(args.key, read_base64_file(args.session_file))
380
    message = args.message_file.read()
381
    plaintext, message_index = session.decrypt(message)
382
383
384
385
    with open(args.session_file, "wb") as f:
        f.write(session.pickle(args.key))
    args.plaintext_file.write(plaintext)

386
387
388
389
390
391
392
393
394
def do_export_inbound_group(args):
    session = InboundGroupSession()
    session.unpickle(args.key, read_base64_file(args.session_file))
    index = args.message_index
    if index is None:
        # default to first known index
        index = session.first_known_index()
    args.export_file.write(session.export_session(index))

395
396
397
398
399
400
401
402
def do_verify_ed25519_signature(args):
    account = Account()
    account.create()
    message = "A Message".encode("ASCII")
    ed25519_key = account.identity_keys()["ed25519"].encode("utf-8")
    signature = account.sign(message)
    ed25519_verify(ed25519_key, message, signature)

403
404
if __name__ == '__main__':
    parser = build_arg_parser()
405
406
    args = parser.parse_args()
    args.func(args)