├── .gitignore ├── .pylint-plugins └── astng_hashlib.py ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── icons ├── 128x128 │ └── keysync.png ├── add.png ├── adium.png ├── chatsecure.png ├── gajim.png ├── gnupg.png ├── irssi.png ├── jitsi.png ├── keysync.icns ├── keysync.ico ├── keysync.png ├── kopete.png ├── pidgin.png └── xchat.png ├── jenkins-build ├── keysync ├── keysync-gui ├── keysync-gui-folder.spec ├── keysync-gui-onefile.spec ├── keysync.desktop ├── man └── keysync.1 ├── otrapps ├── __init__.py ├── adium.py ├── chatsecure.py ├── errors.py ├── gajim.py ├── gnupg.py ├── irssi.py ├── jitsi.py ├── kopete.py ├── otr_fingerprints.py ├── otr_private_key.py ├── pidgin.py ├── util.py └── xchat.py ├── py2app-python2.6.patch ├── setup.py ├── tests ├── SAMENESS │ ├── adium │ │ ├── Accounts.plist │ │ ├── otr.fingerprints │ │ └── otr.private_key │ └── pidgin │ │ ├── accounts.xml │ │ ├── otr.fingerprints │ │ └── otr.private_key ├── adium │ ├── Accounts.plist │ ├── Library │ │ └── Application Support │ │ │ └── Adium 2.0 │ │ │ ├── Login Preferences.plist │ │ │ └── Users │ │ │ └── Default │ │ │ ├── AccountPrefs.plist │ │ │ ├── Accounts.plist │ │ │ ├── ByObjectPrefs.plist │ │ │ ├── Contact Alerts.plist │ │ │ ├── Contact List.plist │ │ │ ├── Event Presets.plist │ │ │ ├── General.plist │ │ │ ├── List Layout.plist │ │ │ ├── List Theme.plist │ │ │ ├── Logging.plist │ │ │ ├── OTR.plist │ │ │ ├── URL Handling Group.plist │ │ │ ├── WebKit Message Display.plist │ │ │ └── libpurple │ │ │ ├── accounts.xml │ │ │ ├── blist.xml │ │ │ ├── prefs.xml │ │ │ └── xmpp-caps.xml │ ├── otr.fingerprints │ └── otr.private_key ├── chatsecure │ ├── otr_keystore │ └── otr_keystore.ofcaes ├── gajim │ ├── config │ ├── guardianproject.info.fpr │ └── guardianproject.info.key3 ├── gnupg │ ├── pubring.gpg │ ├── random_seed │ ├── secring.gpg │ └── trustdb.gpg ├── irssi │ ├── otr.fp │ └── otr.key ├── jitsi │ ├── contactlist.xml │ └── sip-communicator.properties ├── keyczar │ ├── privatekey-v0.5b.json │ ├── privatekey-v0.6b.json │ ├── publickey-v0.5b.json │ └── publickey-v0.6b.json ├── kopete │ ├── fingerprints │ └── privkeys ├── openssl │ ├── parse-private-key-PKCS#8.py │ └── useful-commands.sh ├── pidgin │ ├── accounts.xml │ ├── otr.fingerprints │ └── otr.private_key ├── run-tests.sh └── xchat │ ├── otr.fp │ └── otr.key └── win32 ├── README.md ├── cypherpunks-otr-pubkey.asc ├── fetch-and-verify.sh ├── pidgin-windows-pubkeys.asc ├── pycrypto-pubkey.asc ├── python-pubkeys.asc └── w32-screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | output/ 5 | tests/openssl/*.pem 6 | tests/openssl/*.properties 7 | pylint.parseable 8 | # 9 | win32/files/* 10 | # 11 | MANIFEST 12 | build/ 13 | dist/ 14 | keysync.egg-info/ 15 | # recommended virtualenv folder 16 | env/ 17 | -------------------------------------------------------------------------------- /.pylint-plugins/astng_hashlib.py: -------------------------------------------------------------------------------- 1 | # 2 | # started from http://www.logilab.org/blogentry/78354 3 | # 4 | 5 | from logilab.astng import MANAGER 6 | from logilab.astng.builder import ASTNGBuilder 7 | 8 | def hashlib_transform(module): 9 | if module.name == 'hashlib': 10 | fake = ASTNGBuilder(MANAGER).string_build(''' 11 | 12 | class fakehash(object): 13 | digest_size = -1 14 | def __init__(self, value): pass 15 | def digest(self): 16 | return u'' 17 | def hexdigest(self): 18 | return u'' 19 | def update(self, value): pass 20 | 21 | class md5(fakehash): 22 | pass 23 | 24 | class sha1(fakehash): 25 | pass 26 | 27 | class sha256(fakehash): 28 | pass 29 | 30 | ''') 31 | for hashfunc in ('sha256', 'sha1', 'md5'): 32 | module.locals[hashfunc] = fake.locals[hashfunc] 33 | 34 | def register(linter): 35 | """called when loaded by pylint --load-plugins, register our tranformation 36 | function here 37 | """ 38 | MANAGER.register_transformer(hashlib_transform) 39 | 40 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.txt 3 | include jenkins-build 4 | include keysync.desktop 5 | include keysync-gui-folder.spec 6 | include keysync-gui-onefile.spec 7 | include man/keysync.1 8 | include py2app-python2.6.patch 9 | include pylint.parseable 10 | include icons/128x128/keysync.png 11 | include icons/add.png 12 | include icons/adium.png 13 | include icons/chatsecure.png 14 | include icons/gajim.png 15 | include icons/gnupg.png 16 | include icons/irssi.png 17 | include icons/jitsi.png 18 | include icons/keysync.icns 19 | include icons/keysync.ico 20 | include icons/keysync.png 21 | include icons/kopete.png 22 | include icons/pidgin.png 23 | include icons/xchat.png 24 | include tests/run-tests.sh 25 | include tests/chatsecure/otr_keystore 26 | recursive-include tests/adium *.* 27 | recursive-include tests/gajim *.* 28 | recursive-include tests/gnupg *.* 29 | recursive-include tests/irssi *.* 30 | recursive-include tests/jitsi *.* 31 | recursive-include tests/keyczar *.* 32 | recursive-include tests/openssl *.* 33 | recursive-include tests/pidgin *.* 34 | recursive-include tests/SAMENESS *.* 35 | recursive-include tests/xchat *.* 36 | include win32/cypherpunks-otr-pubkey.asc 37 | include win32/fetch-and-verify.sh 38 | include win32/pidgin-windows-pubkeys.asc 39 | include win32/pycrypto-pubkey.asc 40 | include win32/python-pubkeys.asc 41 | include win32/README.md 42 | include win32/w32-screenshot.png 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | KeySync - One key for all of your chat apps 3 | =========================================== 4 | 5 | There are many chat apps that support OTR encryption for verifying messages. 6 | Many of us use multiple chat apps, including one for desktop and another for 7 | mobile, or one for Mac OS X and another for GNU/Linux or Windows. The trust 8 | relationships are only stored locally in the app in a format that is specific 9 | to that app. Switching between all of them means that you have to manage your 10 | trust relationships for each app that you use. 11 | 12 | KeySync addresses this problem by allowing you to sync your OTR identity and 13 | trust relationships between multiple apps, which eliminates the need to 14 | re-establish those relationships on each new client/device. It currently works 15 | with [ChatSecure] on Android, and [Pidgin], [Adium], and [Jitsi] on desktop. 16 | 17 | Please note: KeySync does not make it easier to have simultaneous encrypted 18 | chats with the same account logged into multiple clients or devices. There 19 | are limitations in the OTR protocol that make this impossible. 20 | 21 | 22 | How to sync 23 | ----------- 24 | 25 | To sync between ChatSecure and your desktop apps, plug in your phone or device 26 | via USB and run the sync. Or you can manually copy the `otr_keystore.ofcaes` 27 | file over to your device's SD Card, where ChatSecure looks for it. Using 28 | ChatSecure, you will need to scan the QRCode that KeySync shows you in order 29 | to complete the sync. The `otr_keystore.ofcaes` file is encrypted to prevent 30 | your private information from leaking out. That QRCode is the password to 31 | your keystore, so do not share it with anyone. 32 | 33 | If you have multiple chat apps that you use, or you are switching from one to 34 | another, you can use KeySync to sync up the trust relationships between 35 | multiple desktop apps. Here's how: 36 | 37 | 1. quit all of the chat apps that you want to 38 | 2. select whichever apps you want to sync 39 | 3. then run the sync 40 | 41 | Now, open your chat apps and you should have synced trust! In case of 42 | problems, it saved your original OTR files in place. They are named using a 43 | long string of numbers that represent the time when they were backed up. 44 | 45 | This is beta software, do not rely on it for strong identity verification. It 46 | is unlikely to mess up so bad as to produce compromised private keys, but 47 | anything is possible. Also, keep in mind that program is handling your 48 | private OTR keys, so make sure that you don't copy, send or email the 49 | `otr_keystore.ofcaes` file somewhere unsafe. All that said, testing 50 | and feedback is greatly appreciated, so we can get it to the point where we 51 | can trust it. 52 | 53 | 54 | Adding apps to KeySync 55 | ---------------------- 56 | 57 | This project has libraries for converting the various OTR file formats between 58 | each other. We have focused on the two major versions: [libotr] format and [otr4j], 59 | and then a few variants of those major formats. All OTR implementations can 60 | be supported as long as they can be read and parsed by Python. 61 | 62 | KeySync has preliminary support for Gajim and IRSSI, and it has a modular 63 | architecture to allow for expansion. If you want to add an app that is not 64 | already included, you just need to make a single python file that converts 65 | between that app's OTR data format and the KeySync internal format (documented 66 | below). 67 | 68 | 69 | Reporting bugs 70 | -------------- 71 | 72 | We appreciate all feedback, be it bug reports, patches, pull requests, feature 73 | requests, etc. Please add your bugs and requests to our issue tracker: 74 | 75 | https://dev.guardianproject.info/projects/keysync/issues 76 | 77 | Email us at support@guardianproject.info with questions, problems, etc., or 78 | just to let us know that you are using KeySync and find it useful. 79 | 80 | 81 | INSTALL 82 | ======= 83 | 84 | KeySync uses lots of Python modules to achieve a smooth syncing 85 | experience. To see a complete list of python modules used on your 86 | platform, see the `dependencies` list in setup.py. Here are some of 87 | the key libraries: 88 | 89 | * BeautifulSoup 4 - http://www.crummy.com/software/BeautifulSoup 90 | * psutil - https://code.google.com/p/psutil 91 | * pure-python-otr - https://github.com/afflux/pure-python-otr 92 | * pyasn1 - http://pyasn1.sourceforge.net/ 93 | * pycrypto - https://www.dlitz.net/software/pycrypto 94 | * pyjavaproperties - http://pypi.python.org/pypi/pyjavaproperties 95 | * pymtp - https://github.com/eighthave/pymtp 96 | * pyparsing - http://pyparsing.wikispaces.com 97 | * python-pgpdump - https://pypi.python.org/pypi/pgpdump 98 | * Python Imaging Library - http://www.pythonware.com/products/pil 99 | * qrcode - https://github.com/lincolnloop/python-qrcode 100 | * Tkinter - https://wiki.python.org/moin/TkInter 101 | 102 | 103 | Debian/Ubuntu/Mint/etc 104 | ---------------------- 105 | 106 | We're working to get all packages into official Debian/Ubuntu/etc. releases, 107 | in the meantime you can install KeySync by adding our PPA (fingerprint 108 | F50EADDD2234F563): 109 | 110 | sudo add-apt-repository ppa:guardianproject/ppa 111 | sudo apt-get update 112 | sudo apt-get install keysync 113 | 114 | PPA URL: https://launchpad.net/~guardianproject/+archive/ppa 115 | 116 | If you want to install the dependencies because you're going to develop 117 | KeySync, then install them manually with: 118 | 119 | sudo apt-get install python-pyparsing python-pyasn1 python-potr python-pymtp \ 120 | python-pyjavaproperties python-beautifulsoup python-qrcode libmtp-dev \ 121 | python-pgpdump python-crypto python-psutil python-tk python-imaging-tk 122 | 123 | For Debian, you can try using the Ubuntu PPA, with something like oneiric for 124 | wheezy, and natty for squeeze: 125 | 126 | deb http://ppa.launchpad.net/guardianproject/ppa/ubuntu oneiric main 127 | 128 | 129 | Mac OS X 130 | -------- 131 | 132 | For Mac OS X, you can download the binary app from our website: 133 | https://guardianproject.info 134 | 135 | You use Brew, Fink, or MacPorts to install pip and virtualenv, then 136 | use those tools to install KeySync (see the *pip+virtualenv install* 137 | section): 138 | 139 | fink install pip-systempython26 virtualenv-systempython26 140 | sudo port install py27-pip py27-virtualenv 141 | 142 | For homebrew, see: https://gist.github.com/pithyless/1208841 143 | 144 | Once you have pip and virtualenv, then you can start to build the 145 | whole thing. First follow the pip+virtualenv instructions below. 146 | Then come back here and do the following in your virtualenv: 147 | 148 | rm -rf build dist 149 | pip install py2app 150 | python setup.py py2app 151 | ls dist/ 152 | 153 | If you are using py2app older than 0.7.4 on Mac OS X 10.6, then you 154 | need to patch py2app to make it work with python2.6: 155 | 156 | pip install --upgrade py2app 157 | patch env/lib/python2.6/site-packages/py2app/build_app.py py2app-python2.6.patch 158 | 159 | 160 | Windows 161 | ------- 162 | 163 | For Windows, you can download the binary app from our website: 164 | https://guardianproject.info 165 | 166 | Build instructions for Windows are in win32/README.md in the source folder. 167 | 168 | Fedora/Redhat/RPMs 169 | ------------------ 170 | 171 | Install these build dependencies locally, then follow the instructions for 172 | pip+virtualenv below: 173 | 174 | sudo yum install gmp-devel tk tkinter python-pillow python-pillow-tk 175 | 176 | 177 | pip+virtualenv install 178 | ------------------ 179 | 180 | Activate your virtual python environment then run pip to install the dependencies: 181 | 182 | virtualenv ./env 183 | . env/bin/activate 184 | pip install -e . 185 | ./keysync 186 | 187 | For a nice step-by-step HOWTO, see: 188 | http://exyr.org/2011/virtualenv-HOWTO/slides.html 189 | 190 | *Note*: Doesn't work on Windows! See win32/README.md 191 | 192 | 193 | ===== 194 | USAGE 195 | ===== 196 | 197 | Currently, the code allows for reading multiple file formats into a python 198 | dictionary form. The only export method currently activated is for ChatSecure 199 | format in a file called otr_keystore.ofcaes. To use, point the `keysync` 200 | script the app that you want to read OTR info from, and it will generate 201 | `otr_keystore` to send to ChatSecure on your Android device (run 202 | `keysync --help` to see all currently available options). 203 | 204 | keysync --input pidgin 205 | 206 | 207 | ======= 208 | FORMATS 209 | ======= 210 | 211 | libotr 212 | ------ 213 | 214 | Adium: 215 | ~/Library/Application Support/Adium 2.0/Users/Default/otr.private_key 216 | ~/Library/Application Support/Adium 2.0/Users/Default/otr.fingerprints 217 | ~/Library/Application Support/Adium 2.0/Users/Default/Accounts.plist 218 | 219 | Uses the standard libotr files and overall file format for 220 | otr.private_key and otr.fingerprints. Account ID is stored as an 221 | integer which must be referenced from the Accounts.plist to get the 222 | actuall XMPP account name (e.g. me@jabber.org). Uses full word 223 | descriptive tags for the various protocols, e.g. libpurple-oscar-AIM, 224 | libpurple-Jabber, etc. 225 | 226 | Pidgin 227 | GNU/Linux 228 | ~/.purple/otr.private_key 229 | ~/.purple/otr.fingerprints 230 | Windows 231 | %APPDATA%\.purple\otr.private_key 232 | %APPDATA%\.purple\otr.fingerprints 233 | 234 | Uses the standard libotr files and overall file format for 235 | otr.private_key and otr.fingerprints. Account IDs are used directly 236 | in the libotr files. XMPP/Jabber Account IDs include the "Resource" 237 | e.g. me@jabber.org/Resource or me@jabber.org/Pidgin. 238 | 239 | irssi 240 | ~/.irssi/otr/otr.key 241 | ~/.irssi/otr/otr.fp 242 | 243 | Uses the standard libotr file format and files, but names the files 244 | differently, basically abbreviated versions of the libotr names. 245 | Account IDs are used directly in the libotr files. 246 | 247 | xchat 248 | ~/.xchat2/otr/otr.key 249 | ~/.xchat2/otr/otr.fp 250 | 251 | Same as irssi 252 | 253 | otr4j 254 | ----- 255 | 256 | ChatSecure: 257 | /data/data/info.guardianproject.otr.app.im/files/otr_keystore 258 | 259 | All OTR information is stored in a single Java .properties 260 | file. Private keys, public key fingerprints, and verification status 261 | are each individual properties. This format also includes the 262 | storage of the remote public keys, unlike libotr. [otr4j] 263 | implementations load the remote public key from the store rather 264 | than always getting it from the OTR session. 265 | 266 | Jitsi: 267 | GNU/Linux 268 | ~/.jitsi/sip-communicator.properties 269 | ~/.jitsi/contactlist.xml 270 | Mac OS X 271 | ~/Library/Application Support/Jitsi/sip-communicator.properties 272 | ~/Library/Application Support/Jitsi/contactlist.xml 273 | Windows 274 | ~/Application Data/Jitsi/sip-communicator.properties 275 | ~/Application Data/Jitsi/contactlist.xml 276 | 277 | All app settings are stored in a single Java .properties file, 278 | including OTR information. Private keys, public key fingerprints, 279 | and verification status are each individual properties. 280 | 281 | 282 | pure-python-otr 283 | --------------- 284 | 285 | pure-python-otr is pure python implementation of the OTR spec. It 286 | includes newer features like Socialist Millionaire's Protocol. The 287 | private key is stored in a separate file per-account. The 288 | fingerprints are stored in the same tab-separated-value format as 289 | [libotr] but with a fingerprint file per-account. 290 | 291 | Gajim: 292 | GNU/Linux: 293 | ~/.local/share/gajim/_SERVERNAME_.key3 294 | ~/.local/share/gajim/_SERVERNAME_.fpr 295 | ~/.config/gajim/config 296 | Windows: 297 | ~/Application Data/Gajim/ 298 | 299 | The private key is serialized in a custom format: p, q, g, y, x, written 300 | consecutively to a file as MPIs. See 301 | [potr/compatcrypto/pycrypto.py](https://github.com/afflux/pure-python-otr/blob/master/src/potr/compatcrypto/pycrypto.py). 302 | 303 | 304 | keyczar 305 | ------- 306 | 307 | KeyCzar stores keys in JSON files with two different formats: 0.5b and 308 | 0.6b. It uses a special base64 encoding with a URL-safe alphabet: 309 | - replaces + 310 | _ replaces / 311 | 312 | http://code.google.com/p/keyczar/wiki/DsaPrivateKey 313 | http://code.google.com/p/keyczar/wiki/DsaPublicKey 314 | 315 | 0.6b 316 | public: 317 | "q": The DSA subprime 318 | "p": The DSA prime 319 | "g": The DSA base 320 | "y": The DSA public key exponent 321 | "size" : The size of the modulus in bits 322 | private: 323 | "publicKey": The JSON representation of the corresponding DsaPublicKey 324 | "x": The secret exponent of this private key 325 | "size" : The size of the modulus in bits 326 | 327 | 0.5b 328 | public: 329 | "x509": A WebSafeBase64 encoded X509 representation 330 | private: 331 | "pkcs8": A WebSafeBase64 encoded PKCS#8 representation of the private key 332 | "publicKey": A WebSafeBase64 encoding of the key's corresponding DsaPublicKey 333 | 334 | 335 | ZRTP 336 | ---- 337 | 338 | A ZID record stores (caches) ZID (ZRTP ID) specific data that helps 339 | ZRTP to achives its key continuity feature. Please refer to the ZRTP 340 | specification to get detailed information about the ZID. 341 | 342 | ZRTP key types: 343 | 2048 bit Diffie-Helman values 344 | 3072 bit Diffie-Helman values 345 | 256 bit Diffie-Helman elliptic curve 346 | 384 bit Diffie-Helman elliptic curve 347 | 348 | 349 | 350 | IMPLEMENTATION 351 | ============== 352 | 353 | Here are some notes on how things are implemented in KeySync. 354 | 355 | Internal key storage 356 | -------------------- 357 | 358 | The key idea in the implementation is to get everything into a common format 359 | internally. That common format can then be handed to any class for a given 360 | program, which knows how to output it to the correct file format. The current 361 | internal data format is a dict of dicts representing a key, called 'keydict'. 362 | So first, you have a dict representing a given account with a given key 363 | associated with it. This account name is used as the unique ID. Then the 364 | whole collection of keys, both local private keys and remote public keys, are 365 | collected in meta dict with the account name as the key and the whole dict as 366 | the value. This format allows for easy merging, which enables syncing between 367 | files. 368 | 369 | Sample structure in python dict notation: 370 | 371 | keydict = { 372 | 'userid': { 373 | 'fingerprint': 'ff66e8c909c4deb51cbd4a02b9e6af4d6af215f8', 374 | 'name': 'userid', 375 | 'protocol': 'IRC', 376 | 'resource': 'laptop', # the XMPP "resource" 377 | 'verification': 'verified', # or 'smp' for Socialist Millionares 378 | 'p': '

', # public part of the DSA key 379 | 'q': '', # public part of the DSA key 380 | 'g': '', # public part of the DSA key 381 | 'x': '', # core of private DSA key 382 | 'y': '', # core of public DSA key 383 | }, 384 | 'userid2' : { ... }, 385 | ... 386 | 'useridn' : { ... } 387 | } 388 | 389 | 390 | Protocol IDs 391 | ------------ 392 | 393 | Unfortunately each app has its own string IDs to represent the different IM 394 | protocols. KeySync uses the Pidgin IDs internally. KeySync is currently 395 | focused on the widely deployed common standards of XMPP and IRC. We welcome 396 | contributions for working with the other protocols. These are the IDs that are 397 | currently working throughout: 398 | 399 | prpl-bonjour XMPP Bonjour (serverless XMPP with mDNS discovery) 400 | prpl-irc IRC (Internet Relay Chat) 401 | prpl-jabber XMPP (Jabber) 402 | 403 | Here is the full list of IDs from Pidgin: 404 | https://developer.pidgin.im/wiki/prpl_id 405 | 406 | 407 | [Adium]: https://adium.im/ 408 | [ChatSecure]: https://chatsecure.org/ 409 | [jitsi]: https://jitsi.org/ 410 | [libotr]: https://otr.cypherpunks.ca/ 411 | [otr4j]: https://github.com/otr4j/otr4j 412 | [pidgin]: https://pidgin.im/ 413 | -------------------------------------------------------------------------------- /icons/128x128/keysync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/128x128/keysync.png -------------------------------------------------------------------------------- /icons/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/add.png -------------------------------------------------------------------------------- /icons/adium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/adium.png -------------------------------------------------------------------------------- /icons/chatsecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/chatsecure.png -------------------------------------------------------------------------------- /icons/gajim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/gajim.png -------------------------------------------------------------------------------- /icons/gnupg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/gnupg.png -------------------------------------------------------------------------------- /icons/irssi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/irssi.png -------------------------------------------------------------------------------- /icons/jitsi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/jitsi.png -------------------------------------------------------------------------------- /icons/keysync.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/keysync.icns -------------------------------------------------------------------------------- /icons/keysync.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/keysync.ico -------------------------------------------------------------------------------- /icons/keysync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/keysync.png -------------------------------------------------------------------------------- /icons/kopete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/kopete.png -------------------------------------------------------------------------------- /icons/pidgin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/pidgin.png -------------------------------------------------------------------------------- /icons/xchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/icons/xchat.png -------------------------------------------------------------------------------- /jenkins-build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # this is the script run by the Jenkins server to run the build and tests. Be 4 | # sure to always run it in its dir, i.e. ./run-tests.sh, otherwise it might 5 | # remove things that you don't want it to. 6 | 7 | if [ `dirname $0` != "." ]; then 8 | echo "only run this script like ./`basename $0`" 9 | exit 10 | fi 11 | 12 | set -e 13 | set -x 14 | 15 | if [ -z $WORKSPACE ]; then 16 | WORKSPACE=`pwd` 17 | fi 18 | 19 | 20 | #------------------------------------------------------------------------------# 21 | # cache pypi downloads 22 | if [ -z $PIP_DOWNLOAD_CACHE ]; then 23 | export PIP_DOWNLOAD_CACHE=$HOME/.pip_download_cache 24 | fi 25 | 26 | #------------------------------------------------------------------------------# 27 | # run local tests 28 | cd $WORKSPACE/tests 29 | ./run-tests.sh 30 | 31 | 32 | #------------------------------------------------------------------------------# 33 | # test install using site packages 34 | rm -rf $WORKSPACE/env 35 | virtualenv --system-site-packages $WORKSPACE/env 36 | . $WORKSPACE/env/bin/activate 37 | pip install -e $WORKSPACE 38 | 39 | # run tests in new pip+virtualenv install 40 | . $WORKSPACE/env/bin/activate 41 | keysync=$WORKSPACE/env/bin/keysync $WORKSPACE/tests/run-tests.sh 42 | 43 | 44 | #------------------------------------------------------------------------------# 45 | # test install using packages from pypi 46 | rm -rf $WORKSPACE/env 47 | virtualenv --no-site-packages $WORKSPACE/env 48 | . $WORKSPACE/env/bin/activate 49 | pip install -e $WORKSPACE 50 | 51 | # run tests in new pip+virtualenv install 52 | . $WORKSPACE/env/bin/activate 53 | keysync=$WORKSPACE/env/bin/keysync $WORKSPACE/tests/run-tests.sh 54 | 55 | 56 | #------------------------------------------------------------------------------# 57 | # run pyflakes 58 | 59 | cd $WORKSPACE 60 | # there are a couple warnings to work around in keysync and keysync-gui 61 | #pyflakes keysync keysync-gui otrapps/*.py setup.py 62 | pyflakes otrapps/*.py setup.py 63 | 64 | 65 | #------------------------------------------------------------------------------# 66 | # run pylint 67 | 68 | cd $WORKSPACE 69 | set +e 70 | # disable E1101 until there is a plugin to handle this properly: 71 | # Module 'sys' has no '_MEIPASS' member 72 | # disable F0401 until there is a plugin to handle this properly: 73 | # keysync-gui:25: [F] Unable to import 'ordereddict' 74 | PYTHONPATH=$WORKSPACE/.pylint-plugins \ 75 | pylint --output-format=parseable --reports=n \ 76 | --disable=E1101,F0401 \ 77 | --load-plugins astng_hashlib \ 78 | otrapps/*.py keysync keysync-gui > $WORKSPACE/pylint.parseable 79 | # only tell jenkins there was an error if we got ERROR or FATAL 80 | [ $(($? & 1)) = "1" ] && exit 1 81 | [ $(($? & 2)) = "2" ] && exit 2 82 | set -e 83 | -------------------------------------------------------------------------------- /keysync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''keysync is a command line program''' 4 | 5 | from __future__ import print_function 6 | import sys 7 | import os 8 | import platform 9 | import argparse 10 | 11 | # if python < 2.7, get OrderedDict from a standalone lib 12 | if sys.version_info[0] == 2 and sys.version_info[1] < 7: 13 | from ordereddict import OrderedDict 14 | else: 15 | from collections import OrderedDict 16 | 17 | import otrapps.util 18 | import otrapps 19 | from otrapps.chatsecure import ChatSecureProperties 20 | 21 | # no argv passed in here because argparse just checks sys.argv directly 22 | def main(): 23 | '''this is the main entry into this program''' 24 | 25 | if len(sys.argv) == 1: 26 | sys.argv.append('--help') # if no args, show help 27 | 28 | # defaults 29 | if platform.system() == 'Darwin': 30 | default_input = 'adium' 31 | else: 32 | default_input = 'pidgin' 33 | 34 | default_output = 'chatsecure' 35 | 36 | 37 | parser = argparse.ArgumentParser() 38 | # Note: the 'default' argument is not used with the input and output args 39 | # below, because the combination of the append action and default, means 40 | # the default will ALWAYS appear in the actions. 41 | # this may or may not be an argparse bug. 42 | parser.add_argument('-i', '--input', action='append', 43 | choices=sorted(otrapps.apps_supported), 44 | help="specify which programs to take as input. if multiple then they'll be merged (default: %s)" % (default_input)) 45 | parser.add_argument('-o', '--output', action='append', 46 | choices=sorted(otrapps.apps_supported), 47 | help="specify which format to write out. if multiple then each will be written out (default: %s)" % (default_output)) 48 | parser.add_argument('--output-folder', default=os.getcwd(), 49 | help='write the output files to this folder (default: current folder)') 50 | parser.add_argument('--no-qrcode', action='store_true', default=False, 51 | help='do not print the ChatSecure QR Code to the terminal') 52 | parser.add_argument('-q', '--quiet', action='store_true', default=False, 53 | help='do not print anything to the terminal') 54 | parser.add_argument('-t', '--test', help=argparse.SUPPRESS, default=None) 55 | parser.add_argument('--version', action='version', version='%(prog)s {v}'.format(v=otrapps.__version__)) 56 | args = parser.parse_args() 57 | 58 | # manually set defaults, see Note above 59 | if args.input == None or len(args.input) == 0: 60 | args.input = [default_input] 61 | if args.output == None or len(args.output) == 0: 62 | args.output = [default_output] 63 | 64 | # downcase all names to be a little more friendly 65 | args.input = [i.lower() for i in args.input] 66 | args.output = [o.lower() for o in args.output] 67 | 68 | keydict = dict() 69 | for app in args.input: 70 | print('Reading %s files...' % ( app )) 71 | # special case GB for now 'cause of this keyfile business 72 | if app == 'chatsecure': 73 | keyfile = os.path.join(args.output_folder, ChatSecureProperties.keyfile) 74 | if os.path.exists(keyfile): 75 | otrapps.util.merge_keydicts(keydict, ChatSecureProperties.parse(keyfile)) 76 | else: 77 | encrypted_keyfile = os.path.join(args.output_folder, ChatSecureProperties.encryptedkeyfile) 78 | if os.path.exists(encrypted_keyfile): 79 | if sys.version_info[0] == 2: 80 | password = raw_input("What is the encryption password for keystore \"otr_keystore.ofcaes\"?\n") 81 | else: 82 | password = input("What is the encryption password for keystore \"otr_keystore.ofcaes\"?\n") 83 | keyfile = ChatSecureProperties._decrypt_ofcaes(encrypted_keyfile, password) 84 | otrapps.util.merge_keydicts(keydict, ChatSecureProperties.parse(keyfile)) 85 | else: 86 | print(('ChatSecure WARNING: No usable "' + ChatSecureProperties.keyfile + 87 | '" or "' + ChatSecureProperties.encryptedkeyfile + 88 | '" file found, not reading keys from ChatSecure!')) 89 | break 90 | 91 | properties = otrapps.apps[app] 92 | if args.test: 93 | # example: "tests/gajim/" 94 | settings_dir = os.path.join(args.test, app) 95 | otrapps.util.merge_keydicts(keydict, properties.parse(settings_dir)) 96 | else: 97 | otrapps.util.merge_keydicts(keydict, properties.parse()) 98 | 99 | if keydict: 100 | keydict = OrderedDict(sorted(keydict.items(), key=lambda t: t[0])) 101 | otrapps.make_outdir(args.output_folder, '') 102 | for app in args.output: 103 | # once again special case GB 104 | if 'chatsecure' in args.output: 105 | ChatSecureProperties.write(keydict, args.output_folder) 106 | if not args.quiet and ChatSecureProperties.password: 107 | if not args.no_qrcode and sys.stdout.isatty(): 108 | print('\nScan this QR Code:') 109 | import qrcode 110 | pwqr = qrcode.QRCode() 111 | pwqr.add_data(ChatSecureProperties.password) 112 | pwqr.print_tty() 113 | print(('\nor enter this password into ChatSecure: \n\t' + ChatSecureProperties.password)) 114 | break 115 | 116 | properties = otrapps.apps[app] 117 | properties.write(keydict, args.output_folder) 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /keysync-gui-folder.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | a = Analysis(['keysync-gui'], 3 | pathex=['c:\\Users\\abel\\Documents\\keysync'], 4 | hiddenimports=[], 5 | hookspath=None, 6 | runtime_hooks=None) 7 | pyz = PYZ(a.pure) 8 | icon_dir = Tree('icons', prefix='icons') 9 | exe = EXE(pyz, 10 | a.scripts, 11 | exclude_binaries=True, 12 | name='KeySync.exe', 13 | icon='icons/keysync.ico', 14 | onefile=True, 15 | debug=False, 16 | strip=None, 17 | upx=True, 18 | console=True) 19 | coll = COLLECT(exe, 20 | a.binaries, 21 | a.zipfiles, 22 | a.datas, 23 | icon_dir, 24 | strip=None, 25 | upx=True, 26 | name='KeySync') 27 | -------------------------------------------------------------------------------- /keysync-gui-onefile.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | a = Analysis(['keysync-gui'], 3 | pathex=['c:\\Users\\abel\\Documents\\keysync'], 4 | hiddenimports=[], 5 | hookspath=None, 6 | runtime_hooks=None) 7 | pyz = PYZ(a.pure) 8 | icon_dir = Tree('icons', prefix='icons') 9 | exe = EXE(pyz, 10 | a.scripts, 11 | a.binaries, 12 | a.zipfiles, 13 | a.datas, 14 | icon_dir, 15 | name='KeySync.exe', 16 | icon='icons/keysync.ico', 17 | debug=False, 18 | strip=None, 19 | upx=True, 20 | console=False) 21 | -------------------------------------------------------------------------------- /keysync.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=KeySync 3 | GenericName=KeySync 4 | Comment=Sync all the OTR trust relationships between multiple chat apps 5 | Exec=keysync-gui 6 | Terminal=false 7 | Type=Application 8 | Icon=keysync 9 | Categories=Utility; 10 | StartupNotify=false 11 | Keywords=secure;private;verified;chat;im;messaging;key;encryption;otr;openpgp;pgp;gpg;gnupg;jabber;xmpp;irc; 12 | -------------------------------------------------------------------------------- /man/keysync.1: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" First parameter, NAME, should be all caps 3 | .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection 4 | .\" other parameters are allowed: see man(7), man(1) 5 | .TH keysync 1 "2012 Apr 21" 6 | .\" Please adjust this date whenever revising the manpage. 7 | .\" 8 | .\" Some roff macros, for reference: 9 | .\" .nh disable hyphenation 10 | .\" .hy enable hyphenation 11 | .\" .ad l left justify 12 | .\" .ad b justify to both left and right margins 13 | .\" .nf disable filling 14 | .\" .fi enable filling 15 | .\" .br insert line break 16 | .\" .sp insert n+1 empty lines 17 | .\" for manpage-specific macros, see man(7) 18 | .SH NAME 19 | keysync \- convert OTR key info between different formats 20 | .SH SYNOPSIS 21 | .B keysync 22 | .RI [options] 23 | .RI [path] 24 | .br 25 | .SH DESCRIPTION 26 | This manual page documents briefly the 27 | .B keysync 28 | command. 29 | .PP 30 | .\" TeX users may be more comfortable with the \fB\fP and 31 | .\" \fI\fP escape sequences to invode bold face and italics, 32 | .\" respectively. 33 | \fBkeysync\fP is a program for converting the various OTR file formats between 34 | each other. KeySync works with the two major OTR keystore versions: libotr 35 | format and otr4j, and then a few variants of those major formats. There is 36 | some preliminary support for pure-python-otr keystores. 37 | 38 | It can read and write the OTR keystore formats from Adium, ChatSecure, IRSSI, 39 | Jitsi, Pidgin, and XChat. It can also read some data from Gajim and GnuPG, 40 | but that is not yet fully implemented. 41 | 42 | For syncing the OTR data to ChatSecure on an Android device, use 43 | \fBkeysync-gui\fP. 44 | 45 | .SH WARNING! 46 | This is beta software, do not rely on it for strong identity verification. It 47 | is unlikely to mess up so bad as to produce compromised secret keys, but 48 | anything is possible. Also, keep in mind that program is handling your 49 | private OTR keys, so careful with the files produced with keysync! That said, 50 | testing and feedback is greatly appreciated, so we can get it to the point 51 | where it can be fully trusted. 52 | .SH OPTIONS 53 | .TP 54 | .B \-h, --help 55 | show this help message and exit 56 | .TP 57 | .B \-i, --input {adium,chatsecure,gajim,gpg,irssi,jitsi,pidgin,xchat} 58 | specify which programs to take as input. if multiple then they'll be merged (default: pidgin) 59 | .TP 60 | .B \-o, --output {adium,chatsecure,gajim,irssi,jitsi,pidgin,xchat} 61 | specify which format to write out. if multiple then 62 | each will be written out (default: chatsecure) 63 | .TP 64 | .B \--output-folder OUTPUT_FOLDER 65 | write the output files to this folder (default: current folder) 66 | .TP 67 | .B \--no-qrcode 68 | do not print the ChatSecure QR Code to the terminal 69 | .TP 70 | .B \-q, --quiet 71 | do not print anything to the terminal 72 | .TP 73 | .B \--version 74 | show program's version number and exit 75 | .SH AUTHOR 76 | keysync was written by The Guardian Project . 77 | .PP 78 | -------------------------------------------------------------------------------- /otrapps/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | '''sets up the otrapps module with all of the currently supported apps''' 3 | 4 | import os 5 | import sys 6 | 7 | from pkg_resources import get_distribution, DistributionNotFound 8 | try: 9 | _dist = get_distribution('keysync') 10 | if not __file__.startswith(os.path.join(_dist.location, 'otrapps')): 11 | raise DistributionNotFound 12 | except DistributionNotFound: 13 | # probably running from source repo or another version is installed 14 | __version__ = '(local version)' 15 | else: 16 | __version__ = _dist.version 17 | 18 | 19 | __all__ = ['adium', 'chatsecure', 'irssi', 'jitsi', 'pidgin', 'gajim', 'gnupg', 'xchat', 'kopete',] 20 | 21 | if __name__ == '__main__': 22 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 23 | import otrapps.adium 24 | import otrapps.chatsecure 25 | import otrapps.irssi 26 | import otrapps.jitsi 27 | import otrapps.pidgin 28 | import otrapps.gajim 29 | import otrapps.gnupg 30 | import otrapps.xchat 31 | import otrapps.kopete 32 | 33 | apps = { 'adium' : otrapps.adium.AdiumProperties, 34 | 'chatsecure': otrapps.chatsecure.ChatSecureProperties, 35 | 'irssi' : otrapps.irssi.IrssiProperties, 36 | 'jitsi' : otrapps.jitsi.JitsiProperties, 37 | 'pidgin' : otrapps.pidgin.PidginProperties, 38 | 'gajim' : otrapps.gajim.GajimProperties, 39 | 'gnupg' : otrapps.gnupg.GnuPGProperties, 40 | 'xchat' : otrapps.xchat.XchatProperties, 41 | 'kopete' : otrapps.kopete.KopeteProperties, 42 | } 43 | apps_supported = apps.keys() 44 | 45 | def make_outdir(output_folder, subdir): 46 | '''create the folder that the results will be written to''' 47 | outdir = os.path.join(output_folder, subdir) 48 | if not os.path.exists(outdir): 49 | os.makedirs(outdir) 50 | return outdir 51 | 52 | 53 | -------------------------------------------------------------------------------- /otrapps/adium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.6 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | import os 6 | import platform 7 | import plistlib 8 | import sys 9 | 10 | if __name__ == '__main__': 11 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 12 | import otrapps.util 13 | from otrapps.otr_private_key import OtrPrivateKeys 14 | from otrapps.otr_fingerprints import OtrFingerprints 15 | 16 | class AdiumProperties(): 17 | 18 | path = os.path.expanduser('~/Library/Application Support/Adium 2.0/Users/Default') 19 | accountsfile = 'Accounts.plist' 20 | keyfile = 'otr.private_key' 21 | fingerprintfile = 'otr.fingerprints' 22 | files = (accountsfile, keyfile, fingerprintfile) 23 | 24 | @staticmethod 25 | def _get_accounts_from_plist(settingsdir): 26 | '''get dict of accounts from Accounts.plist''' 27 | # convert index numbers used for the name into the actual account name 28 | accountsfile = os.path.join(settingsdir, 'Accounts.plist') 29 | print('accountsfile: ', end=' ') 30 | print(accountsfile) 31 | if not os.path.exists(accountsfile): 32 | oldaccountsfile = accountsfile 33 | accountsfile = os.path.join(AdiumProperties.path, AdiumProperties.accountsfile) 34 | if platform.system() == 'Darwin' and os.path.exists(accountsfile): 35 | print('Adium WARNING: "' + oldaccountsfile + '" does not exist! Using:') 36 | print('\t"' + accountsfile + '"') 37 | else: 38 | print('Adium ERROR: No usable Accounts.plist file found, cannot create Adium files!') 39 | return [] 40 | # make sure the plist is in XML format, not binary, 41 | # this should be converted to use python-biplist. 42 | if platform.system() == 'Darwin': 43 | os.system("plutil -convert xml1 '" + accountsfile + "'") 44 | return plistlib.readPlist(accountsfile)['Accounts'] 45 | 46 | @staticmethod 47 | def parse(settingsdir=None): 48 | if settingsdir == None: 49 | settingsdir = AdiumProperties.path 50 | 51 | kf = os.path.join(settingsdir, AdiumProperties.keyfile) 52 | if os.path.exists(kf): 53 | keydict = OtrPrivateKeys.parse(kf) 54 | else: 55 | keydict = dict() 56 | 57 | accounts = AdiumProperties._get_accounts_from_plist(settingsdir) 58 | newkeydict = dict() 59 | for adiumIndex, key in keydict.items(): 60 | for account in accounts: 61 | if account['ObjectID'] == key['name']: 62 | name = account['UID'] 63 | key['name'] = name 64 | newkeydict[name] = key 65 | keydict = newkeydict 66 | 67 | fpf = os.path.join(settingsdir, AdiumProperties.fingerprintfile) 68 | if os.path.exists(fpf): 69 | otrapps.util.merge_keydicts(keydict, OtrFingerprints.parse(fpf)) 70 | 71 | return keydict 72 | 73 | @staticmethod 74 | def write(keydict, savedir='./'): 75 | if not os.path.exists(savedir): 76 | raise Exception('"' + savedir + '" does not exist!') 77 | 78 | # need when converting account names back to Adium's account index number 79 | accountsplist = AdiumProperties._get_accounts_from_plist(savedir) 80 | 81 | kf = os.path.join(savedir, AdiumProperties.keyfile) 82 | adiumkeydict = dict() 83 | for name, key in keydict.items(): 84 | name = key['name'] 85 | for account in accountsplist: 86 | if account['UID'] == name: 87 | key['name'] = account['ObjectID'] 88 | adiumkeydict[name] = key 89 | OtrPrivateKeys.write(keydict, kf) 90 | 91 | accounts = [] 92 | for account in accountsplist: 93 | accounts.append(account['ObjectID']) 94 | fpf = os.path.join(savedir, AdiumProperties.fingerprintfile) 95 | OtrFingerprints.write(keydict, fpf, accounts) 96 | 97 | 98 | if __name__ == '__main__': 99 | 100 | import pprint 101 | 102 | print('Adium stores its files in ' + AdiumProperties.path) 103 | 104 | if len(sys.argv) == 2: 105 | settingsdir = sys.argv[1] 106 | else: 107 | settingsdir = '../tests/adium' 108 | keydict = AdiumProperties.parse(settingsdir) 109 | pprint.pprint(keydict) 110 | 111 | AdiumProperties.write(keydict, '/tmp') 112 | -------------------------------------------------------------------------------- /otrapps/chatsecure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing ChatSecure's OTR key data''' 4 | 5 | from __future__ import print_function 6 | import hashlib 7 | import os 8 | import sys 9 | import pyjavaproperties 10 | import subprocess 11 | import tempfile 12 | 13 | if __name__ == '__main__': 14 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 15 | import otrapps.util 16 | 17 | class ChatSecureProperties(): 18 | 19 | path = '/data/data/info.guardianproject.otr.app.im/files/otr_keystore' 20 | keyfile = 'otr_keystore' 21 | encryptedkeyfile = keyfile + '.ofcaes' 22 | files = (keyfile, encryptedkeyfile) 23 | 24 | password = None 25 | 26 | @staticmethod 27 | def parse(filename): 28 | '''parse the given file into the standard keydict''' 29 | # the parsing and generation is done in separate passes so that 30 | # multiple properties are combined into a single keydict per account, 31 | # containing all of the fields 32 | p = pyjavaproperties.Properties() 33 | p.load(open(filename)) 34 | parsed = [] 35 | for item in p.items(): 36 | propkey = item[0] 37 | if propkey.endswith('.publicKey'): 38 | id = '.'.join(propkey.split('.')[0:-1]) 39 | parsed.append(('public-key', id, item[1])) 40 | elif propkey.endswith('.publicKey.verified'): 41 | keylist = propkey.split('.') 42 | fingerprint = keylist[-3] 43 | id = '.'.join(keylist[0:-3]) 44 | parsed.append(('verified', id, fingerprint)) 45 | elif propkey.endswith('.privateKey'): 46 | id = '.'.join(propkey.split('.')[0:-1]) 47 | parsed.append(('private-key', id, item[1])) 48 | # create blank keys for all IDs 49 | keydict = dict() 50 | for keydata in parsed: 51 | name = keydata[1] 52 | if not name in keydict: 53 | keydict[name] = dict() 54 | keydict[name]['name'] = name 55 | keydict[name]['protocol'] = 'prpl-jabber' 56 | if keydata[0] == 'private-key': 57 | cleaned = keydata[2].replace('\\n', '') 58 | numdict = otrapps.util.ParsePkcs8(cleaned) 59 | for num in ('g', 'p', 'q', 'x'): 60 | keydict[name][num] = numdict[num] 61 | elif keydata[0] == 'verified': 62 | keydict[name]['verification'] = 'verified' 63 | fingerprint = keydata[2].lower() 64 | otrapps.util.check_and_set(keydict[name], 'fingerprint', fingerprint) 65 | elif keydata[0] == 'public-key': 66 | cleaned = keydata[2].replace('\\n', '') 67 | numdict = otrapps.util.ParseX509(cleaned) 68 | for num in ('y', 'g', 'p', 'q'): 69 | keydict[name][num] = numdict[num] 70 | fingerprint = otrapps.util.fingerprint((numdict['y'], numdict['g'], numdict['p'], numdict['q'])) 71 | otrapps.util.check_and_set(keydict[name], 'fingerprint', fingerprint) 72 | return keydict 73 | 74 | @staticmethod 75 | def write(keydict, savedir, password=None): 76 | '''given a keydict, generate a chatsecure file in the savedir''' 77 | p = pyjavaproperties.Properties() 78 | for name, key in keydict.items(): 79 | # only include XMPP keys, since ChatSecure only supports XMPP 80 | # accounts, so we avoid spreading private keys around 81 | if key['protocol'] == 'prpl-jabber' or key['protocol'] == 'prpl-bonjour': 82 | if 'y' in key: 83 | p.setProperty(key['name'] + '.publicKey', otrapps.util.ExportDsaX509(key)) 84 | if 'x' in key: 85 | if not password: 86 | h = hashlib.sha256() 87 | h.update(os.urandom(16)) # salt 88 | h.update(bytes(key['x'])) 89 | password = h.digest().encode('base64') 90 | p.setProperty(key['name'] + '.privateKey', otrapps.util.ExportDsaPkcs8(key)) 91 | if 'fingerprint' in key: 92 | p.setProperty(key['name'] + '.fingerprint', key['fingerprint']) 93 | if 'verification' in key and key['verification'] != None: 94 | p.setProperty(key['name'] + '.' + key['fingerprint'].lower() 95 | + '.publicKey.verified', 'true') 96 | fd, filename = tempfile.mkstemp() 97 | f = os.fdopen(fd, 'w') 98 | p.store(f) 99 | 100 | # if there is no password, then one has not been set, or there 101 | # are not private keys included in the file, so its a lower 102 | # risk file. Encryption only needs to protect the meta data, 103 | # not the private keys. Therefore, its not as bad to generate 104 | # a "random" password here 105 | if not password: 106 | password = os.urandom(32).encode('base64') 107 | 108 | # create passphrase file from the first private key 109 | cmd = ['openssl', 'aes-256-cbc', '-pass', 'stdin', '-in', filename, 110 | '-out', os.path.join(savedir, 'otr_keystore.ofcaes')] 111 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE, 112 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 113 | ChatSecureProperties.password = password 114 | print((p.communicate(password))) 115 | 116 | @staticmethod 117 | def _decrypt_ofcaes(ofcaes_filename, password): 118 | ''' Decrypt an encrypted key file (with user-supplied password).''' 119 | # It might be a bad idea to write out this unencrypted file. 120 | 121 | # get a tmp place to put the decrypted file 122 | fd, filename = tempfile.mkstemp() 123 | f = os.fdopen(fd, 'w') 124 | f.close() 125 | 126 | # same as above, but with the -d flag to decrypt 127 | cmd = ['openssl', 'aes-256-cbc', '-d', '-pass', 'stdin', '-in', ofcaes_filename, 128 | '-out', filename] 129 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE, 130 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 131 | p.communicate(password) 132 | return filename 133 | 134 | 135 | #------------------------------------------------------------------------------# 136 | # for testing from the command line: 137 | def main(argv): 138 | import pprint 139 | 140 | print('ChatSecure stores its files in ' + ChatSecureProperties.path) 141 | 142 | if len(sys.argv) == 2: 143 | settingsfile = sys.argv[1] 144 | else: 145 | settingsfile = '../tests/chatsecure/otr_keystore' 146 | 147 | p = ChatSecureProperties.parse(settingsfile) 148 | print('----------------------------------------') 149 | pprint.pprint(p) 150 | print('----------------------------------------') 151 | 152 | if __name__ == "__main__": 153 | main(sys.argv[1:]) 154 | -------------------------------------------------------------------------------- /otrapps/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | Contains hierarchy of all possible exceptions thrown by Keyczar. 19 | 20 | @author: arkajit.dey@gmail.com (Arkajit Dey) 21 | """ 22 | 23 | class KeyczarError(Exception): 24 | """Indicates exceptions raised by a Keyczar class.""" 25 | 26 | class BadVersionError(KeyczarError): 27 | """Indicates a bad version number was received.""" 28 | 29 | def __init__(self, version): 30 | KeyczarError.__init__(self, 31 | "Received a bad version number: " + str(version)) 32 | 33 | class Base64DecodingError(KeyczarError): 34 | """Indicates an error while performing Base 64 decoding.""" 35 | 36 | class InvalidSignatureError(KeyczarError): 37 | """Indicates an invalid ciphertext signature.""" 38 | 39 | def __init__(self): 40 | KeyczarError.__init__(self, "Invalid ciphertext signature") 41 | 42 | class KeyNotFoundError(KeyczarError): 43 | """Indicates a key with a certain hash id was not found.""" 44 | 45 | def __init__(self, hash): 46 | KeyczarError.__init__(self, 47 | "Key with hash identifier %s not found." % hash) 48 | 49 | class ShortCiphertextError(KeyczarError): 50 | """Indicates a ciphertext too short to be valid.""" 51 | 52 | def __init__(self, length): 53 | KeyczarError.__init__(self, 54 | "Input of length %s is too short to be valid ciphertext." % length) 55 | 56 | class ShortSignatureError(KeyczarError): 57 | """Indicates a signature too short to be valid.""" 58 | 59 | def __init__(self, length): 60 | KeyczarError.__init__(self, 61 | "Input of length %s is too short to be valid signature." % length) 62 | 63 | class NoPrimaryKeyError(KeyNotFoundError): 64 | """Indicates missing primary key.""" 65 | 66 | def __init__(self): 67 | KeyczarError.__init__(self, "No primary key found") 68 | -------------------------------------------------------------------------------- /otrapps/gajim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing Gajim's OTR key data''' 4 | 5 | from __future__ import print_function 6 | import os 7 | import glob 8 | import platform 9 | import sys 10 | import re 11 | import collections 12 | import shutil 13 | 14 | import potr 15 | 16 | if __name__ == '__main__': 17 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 18 | 19 | import otrapps.util 20 | from otrapps.otr_fingerprints import OtrFingerprints 21 | 22 | # the private key is stored in ~/.local/share/gajim/_SERVERNAME_.key_file 23 | # the fingerprints are stored in ~/.local/share/gajim/_SERVERNAME_.fpr 24 | # the accounts are stored in ~/.config/gajim/config 25 | 26 | 27 | class GajimProperties(): 28 | 29 | if platform.system() == 'Windows': 30 | path = os.path.expanduser('~/Application Data/Gajim') 31 | accounts_path = '???' 32 | else: 33 | accounts_file = 'config' 34 | path = os.path.expanduser('~/.local/share/gajim') 35 | accounts_path = os.path.expanduser('~/.config/gajim') 36 | 37 | @staticmethod 38 | def _parse_account_config(accounts_path): 39 | """ 40 | Crudely parses the dot-style config syntax of gajim's config file 41 | """ 42 | if accounts_path is None: 43 | accounts_path = GajimProperties.accounts_path 44 | 45 | accounts_config = os.path.join(accounts_path, GajimProperties.accounts_file) 46 | 47 | keys = ['name', 'hostname', 'resource'] 48 | patterns = [] 49 | for key in keys: 50 | # matches lines like: 51 | # accounts.guardianproject.info.hostname = "guardianproject.info" 52 | patterns.append((key, re.compile('accounts\.(.*)\.%s = (.*)' % key))) 53 | 54 | accounts = collections.defaultdict(dict) 55 | for line in open(accounts_config, 'r'): 56 | for key, pattern in patterns: 57 | for match in re.finditer(pattern, line): 58 | accounts[match.groups()[0]][key] = match.groups()[1] 59 | 60 | return accounts 61 | 62 | @staticmethod 63 | def parse(settingsdir=None): 64 | if settingsdir is None: 65 | settingsdir = GajimProperties.path 66 | accounts_config = GajimProperties.accounts_path 67 | else: 68 | accounts_config = settingsdir 69 | 70 | keydict = dict() 71 | for fpf in glob.glob(os.path.join(settingsdir, '*.fpr')): 72 | print('Reading in ' + fpf) 73 | keys = OtrFingerprints.parse(fpf) 74 | 75 | # replace gajim's 'xmpp' protocol with 'prpl-jabber' that we use in keysync 76 | for key, value in keys.items(): 77 | value['protocol'] = 'prpl-jabber' 78 | keys[key] = value 79 | 80 | otrapps.util.merge_keydicts(keydict, keys) 81 | 82 | accounts = GajimProperties._parse_account_config(accounts_config) 83 | 84 | for key_file in glob.glob(os.path.join(settingsdir, '*.key3')): 85 | account_name = os.path.splitext(os.path.basename(key_file))[0] 86 | if not account_name in accounts.keys(): 87 | print("ERROR found %s not in the account list", key_file) 88 | continue 89 | with open(key_file, 'rb') as key_file: 90 | name = '%s@%s' % (accounts[account_name]['name'], 91 | accounts[account_name]['hostname']) 92 | if name in keydict: 93 | key = keydict[name] 94 | else: 95 | key = dict() 96 | key['name'] = name 97 | key['protocol'] = 'prpl-jabber' 98 | key['resource'] = accounts[account_name]['resource'] 99 | 100 | pk = potr.crypt.PK.parsePrivateKey(key_file.read())[0] 101 | keydata = ['y', 'g', 'p', 'q', 'x'] 102 | for data in keydata: 103 | key[data] = getattr(pk.priv, data) 104 | 105 | key['fingerprint'] = otrapps.util.fingerprint((key['y'], key['g'], 106 | key['p'], key['q'])) 107 | 108 | keydict[key['name']] = key 109 | 110 | return keydict 111 | 112 | @staticmethod 113 | def write(keys, savedir): 114 | if not os.path.exists(savedir): 115 | raise Exception('"' + savedir + '" does not exist!') 116 | 117 | # get existing accounts, we need that to figure out how to call the keys 118 | if os.path.exists(os.path.join(savedir, GajimProperties.accounts_file)): 119 | accountsdir = savedir 120 | elif os.path.exists(os.path.join(GajimProperties.accounts_path, 121 | GajimProperties.accounts_file)): 122 | accountsdir = GajimProperties.accounts_path 123 | else: 124 | raise Exception('Cannot find "' + GajimProperties.accounts_file 125 | + '" in "' + savedir + '"') 126 | accounts = GajimProperties._parse_account_config(accountsdir) 127 | 128 | # now for each account, write the fingerprints and key 129 | accounts_written = set() 130 | for account_name in accounts: 131 | xmpp_name = accounts[account_name]['name'] + '@' + accounts[account_name]['hostname'] 132 | if not xmpp_name in keys: 133 | # no private key for this account, skip it 134 | continue 135 | key = keys[xmpp_name] 136 | if not 'x' in key: 137 | # this is not a private key, nothing to do here 138 | continue 139 | 140 | # write fingerprints. We do this ourselves to make sure we get the right line-endings. 141 | with open(os.path.join(savedir, account_name + '.fpr'), 'w') as fp_file: 142 | for fp_name, fp_key in keys.items(): 143 | if 'fingerprint' in fp_key and 'verification' in fp_key: 144 | row = [fp_name, xmpp_name, 'xmpp', fp_key['fingerprint'], fp_key['verification']] 145 | fp_file.write('\t'.join(row)+'\n') 146 | 147 | # write private key 148 | private_key = potr.compatcrypto.DSAKey((key['y'], key['g'], key['p'], key['q'], key['x']), private=True) 149 | with open(os.path.join(savedir, account_name + '.key3'), 'wb') as key_file: 150 | key_file.write(private_key.serializePrivateKey()) 151 | 152 | print("Wrote key for Gajim:",xmpp_name) 153 | accounts_written.add(xmpp_name) 154 | 155 | # check for unwritten keys 156 | for key_name in keys.keys(): 157 | if 'x' in keys[key_name]: 158 | if key_name not in accounts_written: 159 | print("No Gajim accont found for",key_name+", key has not been written.") 160 | 161 | 162 | #------------------------------------------------------------------------------# 163 | # for testing from the command line: 164 | def main(argv): 165 | import pprint 166 | 167 | print('Gajim stores its files in ' + GajimProperties.path) 168 | 169 | if len(sys.argv) == 2: 170 | settingsdir = sys.argv[1] 171 | else: 172 | settingsdir = '../tests/gajim' 173 | 174 | keydict = GajimProperties.parse(settingsdir) 175 | print('----------------------------------------') 176 | pprint.pprint(keydict) 177 | print('----------------------------------------') 178 | if not os.path.exists(os.path.join('/tmp', GajimProperties.accounts_file)): 179 | shutil.copy(os.path.join(settingsdir, GajimProperties.accounts_file), '/tmp') 180 | GajimProperties.write(keydict, '/tmp') 181 | 182 | if __name__ == "__main__": 183 | main(sys.argv[1:]) 184 | -------------------------------------------------------------------------------- /otrapps/gnupg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | import os 6 | import sys 7 | import pgpdump 8 | 9 | 10 | class GnuPGProperties(): 11 | 12 | path = os.path.expanduser('~/.gnupg') 13 | secring = 'secring.gpg' 14 | pubring = 'pubring.gpg' 15 | files = (secring, pubring) 16 | 17 | @staticmethod 18 | def parse(settingsdir=None): 19 | if settingsdir == None: 20 | settingsdir = GnuPGProperties.path 21 | 22 | secring_file = os.path.join(settingsdir, GnuPGProperties.secring) 23 | if not os.path.exists(secring_file): 24 | return dict() 25 | rawdata = GnuPGProperties.load_data(secring_file) 26 | try: 27 | data = pgpdump.BinaryData(rawdata) 28 | except pgpdump.utils.PgpdumpException, e: 29 | print("gnupg: %s" % (e)) 30 | return dict() 31 | packets = list(data.packets()) 32 | 33 | names = [] 34 | keydict = dict() 35 | for packet in packets: 36 | values = dict() 37 | if isinstance(packet, pgpdump.packet.SecretSubkeyPacket): 38 | if packet.pub_algorithm_type == "dsa": 39 | values['p'] = packet.prime 40 | values['q'] = packet.group_order 41 | values['g'] = packet.group_gen 42 | values['y'] = packet.key_value 43 | values['x'] = packet.exponent_x 44 | # the data comes directly from secret key, mark verified 45 | values['verification'] = 'verified' 46 | values['fingerprint'] = packet.fingerprint 47 | elif isinstance(packet, pgpdump.packet.UserIDPacket): 48 | names.append(str(packet.user_email)) # everything is str, not unicode 49 | if 'fingerprint' in values.keys(): 50 | for name in names: 51 | keydict[name] = values 52 | keydict[name]['name'] = name 53 | keydict[name]['protocol'] = 'prpl-jabber' # assume XMPP for now 54 | return keydict 55 | 56 | @staticmethod 57 | def write(keys, savedir): 58 | print('Writing GnuPG output files is not yet supported!') 59 | 60 | @staticmethod 61 | def load_data(filename): 62 | with open(filename, 'rb') as fileobj: 63 | data = fileobj.read() 64 | return data 65 | 66 | 67 | if __name__ == '__main__': 68 | 69 | import pprint 70 | 71 | print('GnuPG stores its files in ' + GnuPGProperties.path) 72 | 73 | if len(sys.argv) == 2: 74 | settingsdir = sys.argv[1] 75 | else: 76 | settingsdir = '../tests/gnupg' 77 | 78 | l = GnuPGProperties.parse(settingsdir) 79 | pprint.pprint(l) 80 | -------------------------------------------------------------------------------- /otrapps/irssi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing irssi's OTR key data''' 4 | 5 | from __future__ import print_function 6 | import os 7 | import sys 8 | 9 | if __name__ == '__main__': 10 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 11 | import otrapps.util 12 | from otrapps.otr_private_key import OtrPrivateKeys 13 | from otrapps.otr_fingerprints import OtrFingerprints 14 | 15 | class IrssiProperties(): 16 | 17 | path = os.path.expanduser('~/.irssi/otr') 18 | keyfile = 'otr.key' 19 | fingerprintfile = 'otr.fp' 20 | files = (keyfile, fingerprintfile) 21 | 22 | @staticmethod 23 | def parse(settingsdir=None): 24 | if settingsdir == None: 25 | settingsdir = IrssiProperties.path 26 | 27 | kf = os.path.join(settingsdir, IrssiProperties.keyfile) 28 | if os.path.exists(kf): 29 | keydict = OtrPrivateKeys.parse(kf) 30 | else: 31 | keydict = dict() 32 | 33 | fpf = os.path.join(settingsdir, IrssiProperties.fingerprintfile) 34 | if os.path.exists(fpf): 35 | otrapps.util.merge_keydicts(keydict, OtrFingerprints.parse(fpf)) 36 | 37 | return keydict 38 | 39 | @staticmethod 40 | def write(keydict, savedir): 41 | if not os.path.exists(savedir): 42 | raise Exception('"' + savedir + '" does not exist!') 43 | 44 | kf = os.path.join(savedir, IrssiProperties.keyfile) 45 | OtrPrivateKeys.write(keydict, kf) 46 | 47 | accounts = [] 48 | # look for all private keys and use them for the accounts list 49 | for name, key in keydict.items(): 50 | if 'x' in key: 51 | accounts.append(name) 52 | fpf = os.path.join(savedir, IrssiProperties.fingerprintfile) 53 | OtrFingerprints.write(keydict, fpf, accounts) 54 | 55 | 56 | if __name__ == '__main__': 57 | 58 | import pprint 59 | 60 | print('Irssi stores its files in ' + IrssiProperties.path) 61 | 62 | if len(sys.argv) == 2: 63 | settingsdir = sys.argv[1] 64 | else: 65 | settingsdir = '../tests/irssi' 66 | 67 | keydict = IrssiProperties.parse(settingsdir) 68 | pprint.pprint(keydict) 69 | 70 | IrssiProperties.write(keydict, '/tmp') 71 | -------------------------------------------------------------------------------- /otrapps/jitsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing Jitsi's OTR key data''' 4 | 5 | from __future__ import print_function 6 | import os 7 | import platform 8 | import re 9 | import sys 10 | from pyjavaproperties import Properties 11 | from bs4 import BeautifulSoup 12 | 13 | if __name__ == '__main__': 14 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 15 | import otrapps.util 16 | 17 | 18 | # the accounts, private/public keys, and fingerprints are in sip-communicator.properties 19 | # the contacts list is in contactlist.xml 20 | 21 | class JitsiProperties(): 22 | 23 | if platform.system() == 'Darwin': 24 | path = os.path.expanduser('~/Library/Application Support/Jitsi') 25 | elif platform.system() == 'Windows': 26 | path = os.path.expanduser('~/Application Data/Jitsi') 27 | else: 28 | path = os.path.expanduser('~/.jitsi') 29 | propertiesfile = 'sip-communicator.properties' 30 | contactsfile = 'contactlist.xml' 31 | files = (propertiesfile, contactsfile) 32 | 33 | @staticmethod 34 | def _parse_account_uid(uidstring): 35 | username, domain, server = uidstring.split(':')[1].split('@') 36 | return username + '@' + domain 37 | 38 | @staticmethod 39 | def _convert_protocol_name(protocol): 40 | if protocol == 'Jabber': 41 | return 'prpl-jabber' 42 | elif protocol == 'Google Talk': 43 | # this should also mark it as the gtalk variant 44 | return 'prpl-jabber' 45 | else: 46 | return 'IMPLEMENTME' 47 | 48 | @staticmethod 49 | def _parse_account_from_propkey(settingsdir, propkey): 50 | '''give a Java Properties key, parse out a real account UID and 51 | protocol, based on what's listed in the contacts file''' 52 | # jitsi stores the account name in the properties key, so it strips the @ out 53 | m = re.match('net\.java\.sip\.communicator\.plugin\.otr\.(.*)_publicKey.*', propkey) 54 | name_from_prop = '.'.join(m.group(1).split('_')) 55 | # so let's find where the @ was originally placed: 56 | xml = '' 57 | for line in open(os.path.join(settingsdir, JitsiProperties.contactsfile), 'r').readlines(): 58 | xml += line 59 | name = None 60 | protocol = None 61 | for e in BeautifulSoup(xml).find_all('contact'): 62 | if re.match(name_from_prop, e['address']): 63 | name = e['address'] 64 | protocol = JitsiProperties._convert_protocol_name(e['account-id'].split(':')[0]) 65 | break 66 | return str(name), protocol 67 | 68 | 69 | @staticmethod 70 | def parse(settingsdir=None): 71 | if settingsdir == None: 72 | settingsdir = JitsiProperties.path 73 | p = Properties() 74 | p.load(open(os.path.join(settingsdir, JitsiProperties.propertiesfile))) 75 | keydict = dict() 76 | for item in p.items(): 77 | propkey = item[0] 78 | name = '' 79 | if re.match('net\.java\.sip\.communicator\.impl\.protocol\.jabber\.acc[0-9]+\.ACCOUNT_UID', propkey): 80 | name = JitsiProperties._parse_account_uid(item[1]) 81 | if name in keydict: 82 | key = keydict[name] 83 | else: 84 | key = dict() 85 | key['name'] = name 86 | key['protocol'] = 'prpl-jabber' 87 | keydict[name] = key 88 | 89 | propkey_base = ('net.java.sip.communicator.plugin.otr.' 90 | + re.sub('[^a-zA-Z0-9_]', '_', item[1])) 91 | private_key = p.getProperty(propkey_base + '_privateKey').strip() 92 | public_key = p.getProperty(propkey_base + '_publicKey').strip() 93 | numdict = otrapps.util.ParsePkcs8(private_key) 94 | key['x'] = numdict['x'] 95 | numdict = otrapps.util.ParseX509(public_key) 96 | for num in ('y', 'g', 'p', 'q'): 97 | key[num] = numdict[num] 98 | key['fingerprint'] = otrapps.util.fingerprint((key['y'], key['g'], key['p'], key['q'])) 99 | verifiedkey = ('net.java.sip.communicator.plugin.otr.' 100 | + re.sub('[^a-zA-Z0-9_]', '_', key['name']) 101 | + '_publicKey_verified') 102 | if p.getProperty(verifiedkey).strip() == 'true': 103 | key['verification'] = 'verified' 104 | elif (re.match('net\.java\.sip\.communicator\.plugin\.otr\..*_publicKey_verified', propkey)): 105 | name, protocol = JitsiProperties._parse_account_from_propkey(settingsdir, propkey) 106 | if name != None: 107 | if name in keydict: 108 | key = keydict[name] 109 | else: 110 | key = dict() 111 | key['name'] = name 112 | keydict[name] = key 113 | if protocol and 'protocol' not in keydict[name]: 114 | key['protocol'] = protocol 115 | key['verification'] = 'verified' 116 | # if the protocol name is included in the property name, its a local account with private key 117 | elif (re.match('net\.java\.sip\.communicator\.plugin\.otr\..*_publicKey', propkey) and not 118 | re.match('net\.java\.sip\.communicator\.plugin\.otr\.(Jabber_|Google_Talk_)', propkey)): 119 | name, ignored = JitsiProperties._parse_account_from_propkey(settingsdir, propkey) 120 | if name in keydict: 121 | key = keydict[name] 122 | else: 123 | key = dict() 124 | key['name'] = name 125 | key['protocol'] = 'prpl-jabber' 126 | keydict[name] = key 127 | numdict = otrapps.util.ParseX509(item[1]) 128 | for num in ('y', 'g', 'p', 'q'): 129 | key[num] = numdict[num] 130 | key['fingerprint'] = otrapps.util.fingerprint((key['y'], key['g'], key['p'], key['q'])) 131 | return keydict 132 | 133 | @staticmethod 134 | def write(keydict, savedir): 135 | if not os.path.exists(savedir): 136 | raise Exception('"' + savedir + '" does not exist!') 137 | 138 | loadfile = os.path.join(savedir, JitsiProperties.propertiesfile) 139 | savefile = loadfile 140 | if not os.path.exists(loadfile) and os.path.exists(JitsiProperties.path): 141 | print('Jitsi NOTICE: "' + loadfile + '" does not exist! Reading from:') 142 | loadfile = os.path.join(JitsiProperties.path, JitsiProperties.propertiesfile) 143 | print('\t"' + loadfile + '"') 144 | 145 | propkey_base = 'net.java.sip.communicator.plugin.otr.' 146 | p = Properties() 147 | p.load(open(loadfile)) 148 | for name, key in keydict.items(): 149 | if 'verification' in key and key['verification'] != '': 150 | verifiedkey = (propkey_base + re.sub('[^a-zA-Z0-9_]', '_', key['name']) 151 | + '_publicKey_verified') 152 | p[verifiedkey] = 'true' 153 | if 'y' in key: 154 | pubkey = (propkey_base + re.sub('[^a-zA-Z0-9_]', '_', key['name']) 155 | + '_publicKey') 156 | p.setProperty(pubkey, otrapps.util.ExportDsaX509(key)) 157 | if 'x' in key: 158 | protocol_id = 'UNKNOWN_' 159 | domain_id = 'unknown' 160 | servername = None 161 | if '@' in key['name']: 162 | domainname = key['name'].split('@')[1] 163 | domain_id = re.sub('[^a-zA-Z0-9_]', '_', domainname) 164 | if domainname == 'chat.facebook.com': 165 | protocol_id = 'Facebook_' 166 | elif domainname == 'gmail.com' \ 167 | or domainname == 'google.com' \ 168 | or domainname == 'googlemail.com': 169 | protocol_id = 'Google_Talk_' 170 | servername = 'talk_google_com' 171 | else: 172 | protocol_id = 'Jabber_' 173 | else: 174 | if key['protocol'] == 'prpl-icq': 175 | protocol_id = 'ICQ_' 176 | domain_id = 'icq_com' 177 | elif key['protocol'] == 'prpl-yahoo': 178 | protocol_id = 'Yahoo__' 179 | domain_id = 'yahoo_com' 180 | # Writing 181 | pubkey = (propkey_base + protocol_id + re.sub('[^a-zA-Z0-9_]', '_', key['name']) 182 | + '_' + domain_id + '_publicKey') 183 | p.setProperty(pubkey, otrapps.util.ExportDsaX509(key)) 184 | privkey = (propkey_base + protocol_id + re.sub('[^a-zA-Z0-9_]', '_', key['name']) 185 | + '_' + domain_id + '_privateKey') 186 | p.setProperty(privkey, otrapps.util.ExportDsaPkcs8(key)) 187 | 188 | if servername: 189 | pubkey = (propkey_base + protocol_id + re.sub('[^a-zA-Z0-9_]', '_', key['name']) 190 | + '_' + servername + '_publicKey') 191 | p.setProperty(pubkey, otrapps.util.ExportDsaX509(key)) 192 | privkey = (propkey_base + protocol_id + re.sub('[^a-zA-Z0-9_]', '_', key['name']) 193 | + '_' + servername + '_privateKey') 194 | p.setProperty(privkey, otrapps.util.ExportDsaPkcs8(key)) 195 | 196 | p.store(open(savefile, 'w')) 197 | 198 | 199 | 200 | #------------------------------------------------------------------------------# 201 | # for testing from the command line: 202 | def main(argv): 203 | import pprint 204 | 205 | print('Jitsi stores its files in ' + JitsiProperties.path) 206 | 207 | if len(sys.argv) == 2: 208 | settingsdir = sys.argv[1] 209 | else: 210 | settingsdir = '../tests/jitsi' 211 | 212 | p = JitsiProperties.parse(settingsdir) 213 | print('----------------------------------------') 214 | pprint.pprint(p) 215 | print('----------------------------------------') 216 | 217 | if __name__ == "__main__": 218 | main(sys.argv[1:]) 219 | -------------------------------------------------------------------------------- /otrapps/kopete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing Kopete's OTR key data''' 4 | 5 | from __future__ import print_function 6 | import os 7 | import sys 8 | 9 | if __name__ == '__main__': 10 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 11 | import otrapps.util 12 | from otrapps.otr_private_key import OtrPrivateKeys 13 | from otrapps.otr_fingerprints import OtrFingerprints 14 | 15 | class KopeteProperties(): 16 | 17 | path = os.path.expanduser('~/.kde/share/apps/kopete_otr') 18 | keyfile = 'privkeys' 19 | fingerprintfile = 'fingerprints' 20 | files = (keyfile, fingerprintfile) 21 | 22 | @staticmethod 23 | def _convert_protocol_name(protocol): 24 | if protocol == 'Jabber': 25 | return 'prpl-jabber' 26 | elif protocol == 'prpl-jabber': 27 | return 'Jabber' 28 | elif protocol == 'Google Talk': 29 | # this should also mark it as the gtalk variant 30 | return 'prpl-jabber' 31 | else: 32 | print('IMPLEMENTME') 33 | print(protocol) 34 | return 'IMPLEMENTME' 35 | 36 | @staticmethod 37 | def parse(settingsdir=None): 38 | if settingsdir == None: 39 | settingsdir = KopeteProperties.path 40 | 41 | kf = os.path.join(settingsdir, KopeteProperties.keyfile) 42 | if os.path.exists(kf): 43 | keydict = OtrPrivateKeys.parse(kf) 44 | for key in keydict: 45 | for value in keydict[key]: 46 | if value == 'protocol': 47 | keydict[key][value] = KopeteProperties._convert_protocol_name(keydict[key][value]) 48 | else: 49 | keydict = dict() 50 | 51 | fpf = os.path.join(settingsdir, KopeteProperties.fingerprintfile) 52 | if os.path.exists(fpf): 53 | tmpdict = OtrFingerprints.parse(fpf) 54 | for key in tmpdict: 55 | for value in tmpdict[key]: 56 | if value == 'protocol': 57 | tmpdict[key][value] = KopeteProperties._convert_protocol_name(tmpdict[key][value]) 58 | 59 | otrapps.util.merge_keydicts(keydict, tmpdict) 60 | 61 | return keydict 62 | 63 | @staticmethod 64 | def write(keydict, savedir): 65 | if not os.path.exists(savedir): 66 | raise Exception('"' + savedir + '" does not exist!') 67 | 68 | for key in keydict: 69 | for value in keydict[key]: 70 | if value == 'protocol': 71 | keydict[key][value] = KopeteProperties._convert_protocol_name(keydict[key][value]) 72 | 73 | kf = os.path.join(savedir, KopeteProperties.keyfile) 74 | OtrPrivateKeys.write(keydict, kf) 75 | 76 | accounts = [] 77 | # look for all private keys and use them for the accounts list 78 | for name, key in keydict.items(): 79 | if 'x' in key: 80 | accounts.append(name) 81 | fpf = os.path.join(savedir, KopeteProperties.fingerprintfile) 82 | OtrFingerprints.write(keydict, fpf, accounts) 83 | 84 | 85 | 86 | if __name__ == '__main__': 87 | 88 | import pprint 89 | 90 | print('Kopete stores its files in ' + KopeteProperties.path) 91 | 92 | if len(sys.argv) == 2: 93 | settingsdir = sys.argv[1] 94 | else: 95 | settingsdir = '../tests/kopete' 96 | 97 | keydict = KopeteProperties.parse(settingsdir) 98 | pprint.pprint(keydict) 99 | 100 | KopeteProperties.write(keydict, '/tmp') 101 | -------------------------------------------------------------------------------- /otrapps/otr_fingerprints.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing libotr's public key data''' 4 | 5 | from __future__ import print_function 6 | import csv 7 | 8 | class OtrFingerprints(): 9 | 10 | @staticmethod 11 | def parse(filename): 12 | '''parse the otr.fingerprints file and return a list of keydicts''' 13 | tsv = csv.reader(open(filename, 'r'), delimiter='\t') 14 | keydict = dict() 15 | for row in tsv: 16 | key = dict() 17 | name = row[0].strip() 18 | key['name'] = name 19 | key['protocol'] = row[2].strip() 20 | key['fingerprint'] = row[3].strip() 21 | key['verification'] = row[4].strip() 22 | keydict[name] = key 23 | return keydict 24 | 25 | @staticmethod 26 | def _includexmppresource(accounts, resources): 27 | '''pidgin requires the XMPP Resource in the name of the associated account''' 28 | returnlist = [] 29 | for account in accounts: 30 | if account in resources.keys(): 31 | returnlist.append(account + '/' + resources[account]) 32 | else: 33 | returnlist.append(account + '/' + 'ReplaceMeWithActualXMPPResource') 34 | return returnlist 35 | 36 | @staticmethod 37 | def write(keydict, filename, accounts, resources=None): 38 | if resources: 39 | accounts = OtrFingerprints._includexmppresource(accounts, resources) 40 | # we have to use this list 'accounts' rather than the private 41 | # keys in the keydict in order to support apps like Adium that 42 | # don't use the actual account ID as the index in the files. 43 | tsv = csv.writer(open(filename, 'w'), delimiter='\t') 44 | for name, key in keydict.items(): 45 | if 'fingerprint' in key: 46 | for account in accounts: 47 | row = [name, account, key['protocol'], key['fingerprint']] 48 | if 'verification' in key and key['verification'] != None: 49 | row.append(key['verification']) 50 | tsv.writerow(row) 51 | 52 | 53 | if __name__ == '__main__': 54 | 55 | import sys 56 | import pprint 57 | keydict = OtrFingerprints.parse(sys.argv[1]) 58 | pprint.pprint(keydict) 59 | accounts = [ 'gptest@jabber.org', 'gptest@limun.org', 'hans@eds.org'] 60 | OtrFingerprints.write(keydict, 'otr.fingerprints', accounts) 61 | -------------------------------------------------------------------------------- /otrapps/otr_private_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing libotr's secret key data''' 4 | 5 | from __future__ import print_function 6 | from pyparsing import Forward, Group, OneOrMore, Optional, ParseFatalException, \ 7 | Suppress, Word, ZeroOrMore, alphanums, dblQuotedString, hexnums, nums, \ 8 | printables, removeQuotes 9 | from base64 import b64decode 10 | import sys 11 | 12 | if __name__ == '__main__': 13 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 14 | import otrapps.util 15 | 16 | class OtrPrivateKeys(): 17 | 18 | @staticmethod 19 | def verifyLen(t): 20 | t = t[0] 21 | if t.len is not None: 22 | t1len = len(t[1]) 23 | if t1len != t.len: 24 | raise ParseFatalException, \ 25 | "invalid data of length %d, expected %s" % (t1len, t.len) 26 | return t[1] 27 | 28 | @staticmethod 29 | def parse_sexp(data): 30 | '''parse sexp/S-expression format and return a python list''' 31 | # define punctuation literals 32 | LPAR, RPAR, LBRK, RBRK, LBRC, RBRC, VBAR = map(Suppress, "()[]{}|") 33 | 34 | decimal = Word("123456789", nums).setParseAction(lambda t: int(t[0])) 35 | bytes = Word(printables) 36 | raw = Group(decimal.setResultsName("len") + Suppress(":") + bytes).setParseAction(OtrPrivateKeys.verifyLen) 37 | token = Word(alphanums + "-./_:*+=") 38 | base64_ = Group(Optional(decimal, default=None).setResultsName("len") + VBAR 39 | + OneOrMore(Word( alphanums +"+/=" )).setParseAction(lambda t: b64decode("".join(t))) 40 | + VBAR).setParseAction(OtrPrivateKeys.verifyLen) 41 | 42 | hexadecimal = ("#" + OneOrMore(Word(hexnums)) + "#")\ 43 | .setParseAction(lambda t: int("".join(t[1:-1]),16)) 44 | qString = Group(Optional(decimal, default=None).setResultsName("len") + 45 | dblQuotedString.setParseAction(removeQuotes)).setParseAction(OtrPrivateKeys.verifyLen) 46 | simpleString = raw | token | base64_ | hexadecimal | qString 47 | 48 | display = LBRK + simpleString + RBRK 49 | string_ = Optional(display) + simpleString 50 | 51 | sexp = Forward() 52 | sexpList = Group(LPAR + ZeroOrMore(sexp) + RPAR) 53 | sexp << ( string_ | sexpList ) 54 | 55 | try: 56 | sexpr = sexp.parseString(data) 57 | return sexpr.asList()[0][1:] 58 | except ParseFatalException, pfe: 59 | print("Error:", pfe.msg) 60 | print(pfe.loc) 61 | print(pfe.markInputline()) 62 | 63 | @staticmethod 64 | def parse(filename): 65 | '''parse the otr.private_key S-Expression and return an OTR dict''' 66 | 67 | f = open(filename, 'r') 68 | data = "" 69 | for line in f.readlines(): 70 | data += line 71 | f.close() 72 | 73 | sexplist = OtrPrivateKeys.parse_sexp(data) 74 | keydict = dict() 75 | for sexpkey in sexplist: 76 | if sexpkey[0] == "account": 77 | key = dict() 78 | name = '' 79 | for element in sexpkey: 80 | # 'name' must be the first element in the sexp or BOOM! 81 | if element[0] == "name": 82 | if element[1].find('/') > -1: 83 | name, resource = element[1].split('/') 84 | else: 85 | name = element[1].strip() 86 | resource = '' 87 | key = dict() 88 | key['name'] = name.strip() 89 | key['resource'] = resource.strip() 90 | if element[0] == "protocol": 91 | key['protocol'] = element[1] 92 | elif element[0] == "private-key": 93 | if element[1][0] == 'dsa': 94 | key['type'] = 'dsa' 95 | for num in element[1][1:6]: 96 | key[num[0]] = num[1] 97 | keytuple = (key['y'], key['g'], key['p'], key['q']) 98 | key['fingerprint'] = otrapps.util.fingerprint(keytuple) 99 | keydict[name] = key 100 | return keydict 101 | 102 | @staticmethod 103 | def _getaccountname(key, resources): 104 | if resources: 105 | # pidgin requires the XMPP Resource in the account name for otr.private_keys 106 | if key['protocol'] == 'prpl-jabber' and 'x' in key.keys(): 107 | name = key['name'] 108 | if name in resources.keys(): 109 | return key['name'] + '/' + resources[name] 110 | else: 111 | return key['name'] + '/' + 'ReplaceMeWithActualXMPPResource' 112 | return key['name'] 113 | 114 | @staticmethod 115 | def write(keydict, filename, resources=None): 116 | privkeys = '(privkeys\n' 117 | for name, key in keydict.items(): 118 | if 'x' in key: 119 | dsa = ' (p #' + ('%0258X' % key['p']) + '#)\n' 120 | dsa += ' (q #' + ('%042X' % key['q']) + '#)\n' 121 | dsa += ' (g #' + ('%0258X' % key['g']) + '#)\n' 122 | dsa += ' (y #' + ('%0256X' % key['y']) + '#)\n' 123 | dsa += ' (x #' + ('%042X' % key['x']) + '#)\n' 124 | account = OtrPrivateKeys._getaccountname(key, resources) 125 | contents = ('(name "' + account + '")\n' + 126 | '(protocol ' + key['protocol'] + ')\n' + 127 | '(private-key \n (dsa \n' + dsa + ' )\n )\n') 128 | privkeys += ' (account\n' + contents + ' )\n' 129 | privkeys += ')\n' 130 | f = open(filename, 'w') 131 | f.write(privkeys) 132 | f.close() 133 | 134 | if __name__ == "__main__": 135 | import sys 136 | import pprint 137 | 138 | pp = pprint.PrettyPrinter(indent=4) 139 | pp.pprint(OtrPrivateKeys.parse(sys.argv[1])) 140 | -------------------------------------------------------------------------------- /otrapps/pidgin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing Pidgin's OTR key data''' 4 | 5 | from __future__ import print_function 6 | import os 7 | import sys 8 | from bs4 import BeautifulSoup 9 | 10 | if __name__ == '__main__': 11 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 12 | import otrapps.util 13 | from otrapps.otr_private_key import OtrPrivateKeys 14 | from otrapps.otr_fingerprints import OtrFingerprints 15 | 16 | class PidginProperties(): 17 | 18 | if sys.platform == 'win32': 19 | path = os.path.join(os.environ.get('APPDATA'), '.purple') 20 | else: 21 | path = os.path.expanduser('~/.purple') 22 | accountsfile = 'accounts.xml' 23 | keyfile = 'otr.private_key' 24 | fingerprintfile = 'otr.fingerprints' 25 | 26 | @staticmethod 27 | def _get_resources(settingsdir): 28 | '''parse out the XMPP Resource from every Pidgin account''' 29 | resources = dict() 30 | accountsfile = os.path.join(settingsdir, PidginProperties.accountsfile) 31 | if not os.path.exists(accountsfile): 32 | print('Pidgin WARNING: No usable accounts.xml file found, add XMPP Resource to otr.private_key by hand!') 33 | return resources 34 | xml = '' 35 | for line in open(accountsfile, 'r').readlines(): 36 | xml += line 37 | for e in BeautifulSoup(xml)(text='prpl-jabber'): 38 | pidginname = e.parent.parent.find('name').contents[0].split('/') 39 | name = pidginname[0] 40 | if len(pidginname) == 2: 41 | resources[name] = pidginname[1] 42 | else: 43 | # Pidgin requires an XMPP Resource, even if its blank 44 | resources[name] = '' 45 | return resources 46 | 47 | @staticmethod 48 | def parse(settingsdir=None): 49 | if settingsdir == None: 50 | settingsdir = PidginProperties.path 51 | 52 | kf = os.path.join(settingsdir, PidginProperties.keyfile) 53 | if os.path.exists(kf): 54 | keydict = OtrPrivateKeys.parse(kf) 55 | else: 56 | keydict = dict() 57 | 58 | fpf = os.path.join(settingsdir, PidginProperties.fingerprintfile) 59 | if os.path.exists(fpf): 60 | otrapps.util.merge_keydicts(keydict, OtrFingerprints.parse(fpf)) 61 | 62 | resources = PidginProperties._get_resources(settingsdir) 63 | for name, key in keydict.items(): 64 | if key['protocol'] == 'prpl-jabber' \ 65 | and 'x' in key.keys() \ 66 | and name in resources.keys(): 67 | key['resource'] = resources[name] 68 | 69 | return keydict 70 | 71 | @staticmethod 72 | def write(keydict, savedir): 73 | if not os.path.exists(savedir): 74 | raise Exception('"' + savedir + '" does not exist!') 75 | 76 | kf = os.path.join(savedir, PidginProperties.keyfile) 77 | # Pidgin requires the XMPP resource in the account name field of the 78 | # OTR private keys file, so fetch it from the existing account info 79 | if os.path.exists(os.path.join(savedir, PidginProperties.accountsfile)): 80 | accountsdir = savedir 81 | elif os.path.exists(os.path.join(PidginProperties.path, 82 | PidginProperties.accountsfile)): 83 | accountsdir = PidginProperties.path 84 | else: 85 | raise Exception('Cannot find "' + PidginProperties.accountsfile 86 | + '" in "' + savedir + '"') 87 | resources = PidginProperties._get_resources(accountsdir) 88 | OtrPrivateKeys.write(keydict, kf, resources=resources) 89 | 90 | accounts = [] 91 | # look for all private keys and use them for the accounts list 92 | for name, key in keydict.items(): 93 | if 'x' in key: 94 | accounts.append(name) 95 | fpf = os.path.join(savedir, PidginProperties.fingerprintfile) 96 | OtrFingerprints.write(keydict, fpf, accounts, resources=resources) 97 | 98 | 99 | if __name__ == '__main__': 100 | 101 | import pprint 102 | import shutil 103 | 104 | print('Pidgin stores its files in ' + PidginProperties.path) 105 | 106 | if len(sys.argv) == 2: 107 | settingsdir = sys.argv[1] 108 | else: 109 | settingsdir = '../tests/pidgin' 110 | 111 | keydict = PidginProperties.parse(settingsdir) 112 | pprint.pprint(keydict) 113 | 114 | if not os.path.exists(os.path.join('/tmp', PidginProperties.accountsfile)): 115 | shutil.copy(os.path.join(settingsdir, PidginProperties.accountsfile), 116 | '/tmp') 117 | PidginProperties.write(keydict, '/tmp') 118 | -------------------------------------------------------------------------------- /otrapps/util.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2008 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Utility functions for keyczar package modified to use standard base64 18 | format that is commonly used for OTR keys. 19 | 20 | @author: arkajit.dey@gmail.com (Arkajit Dey) 21 | @author: hans@eds.org (Hans-Christoph Steiner) 22 | """ 23 | 24 | from __future__ import print_function 25 | import base64 26 | import math 27 | import os 28 | import psutil 29 | import re 30 | import signal 31 | import sys 32 | import tempfile 33 | try: 34 | # Import hashlib if Python >= 2.5 35 | from hashlib import sha1 36 | assert sha1 # silence pyflakes 37 | except ImportError: 38 | from sha import sha as sha1 39 | 40 | from pyasn1.codec.der import decoder 41 | from pyasn1.codec.der import encoder 42 | from pyasn1.type import univ 43 | 44 | from potr.utils import bytes_to_long 45 | from potr.compatcrypto import DSAKey 46 | 47 | if __name__ == '__main__': 48 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 49 | import otrapps.errors as errors 50 | 51 | 52 | # gracefully handle it when pymtp doesn't exist 53 | class MTPDummy(): 54 | def detect_devices(self): 55 | return [] 56 | try: 57 | import pymtp 58 | mtp = pymtp.MTP() 59 | except: 60 | mtp = MTPDummy() 61 | # GNOME GVFS mount point for MTP devices 62 | 63 | if sys.platform != 'win32': 64 | # this crashes windows in the ntpath sys lib 65 | mtp.gvfs_mountpoint = os.path.join(os.getenv('HOME'), '.gvfs', 'mtp') 66 | 67 | 68 | HLEN = sha1().digest_size # length of the hash output 69 | 70 | #RSAPrivateKey ::= SEQUENCE { 71 | # version Version, 72 | # modulus INTEGER, -- n 73 | # publicExponent INTEGER, -- e 74 | # privateExponent INTEGER, -- d 75 | # prime1 INTEGER, -- p 76 | # prime2 INTEGER, -- q 77 | # exponent1 INTEGER, -- d mod (p-1) 78 | # exponent2 INTEGER, -- d mod (q-1) 79 | # coefficient INTEGER -- (inverse of q) mod p } 80 | # 81 | #Version ::= INTEGER 82 | RSA_OID = univ.ObjectIdentifier('1.2.840.113549.1.1.1') 83 | RSA_PARAMS = ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'invq'] 84 | DSA_OID = univ.ObjectIdentifier('1.2.840.10040.4.1') 85 | DSA_PARAMS = ['p', 'q', 'g'] # only algorithm params, not public/private keys 86 | SHA1RSA_OID = univ.ObjectIdentifier('1.2.840.113549.1.1.5') 87 | SHA1_OID = univ.ObjectIdentifier('1.3.14.3.2.26') 88 | 89 | def ASN1Sequence(*vals): 90 | seq = univ.Sequence() 91 | for i in range(len(vals)): 92 | seq.setComponentByPosition(i, vals[i]) 93 | return seq 94 | 95 | def ParseASN1Sequence(seq): 96 | return [seq.getComponentByPosition(i) for i in range(len(seq))] 97 | 98 | #PrivateKeyInfo ::= SEQUENCE { 99 | # version Version, 100 | # 101 | # privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, 102 | # privateKey PrivateKey, 103 | # attributes [0] IMPLICIT Attributes OPTIONAL } 104 | # 105 | #Version ::= INTEGER 106 | # 107 | #PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier 108 | # 109 | #PrivateKey ::= OCTET STRING 110 | # 111 | #Attributes ::= SET OF Attribute 112 | def ParsePkcs8(pkcs8): 113 | seq = ParseASN1Sequence(decoder.decode(Decode(pkcs8))[0]) 114 | if len(seq) != 3: # need three fields in PrivateKeyInfo 115 | raise errors.KeyczarError("Illegal PKCS8 String.") 116 | version = int(seq[0]) 117 | if version != 0: 118 | raise errors.KeyczarError("Unrecognized PKCS8 Version") 119 | [oid, alg_params] = ParseASN1Sequence(seq[1]) 120 | key = decoder.decode(seq[2])[0] 121 | # Component 2 is an OCTET STRING which is further decoded 122 | params = {} 123 | if oid == RSA_OID: 124 | key = ParseASN1Sequence(key) 125 | version = int(key[0]) 126 | if version != 0: 127 | raise errors.KeyczarError("Unrecognized RSA Private Key Version") 128 | for i in range(len(RSA_PARAMS)): 129 | params[RSA_PARAMS[i]] = long(key[i+1]) 130 | elif oid == DSA_OID: 131 | alg_params = ParseASN1Sequence(alg_params) 132 | for i in range(len(DSA_PARAMS)): 133 | params[DSA_PARAMS[i]] = long(alg_params[i]) 134 | params['x'] = long(key) 135 | else: 136 | raise errors.KeyczarError("Unrecognized AlgorithmIdentifier: not RSA/DSA") 137 | return params 138 | 139 | def ExportRsaPkcs8(params): 140 | oid = ASN1Sequence(RSA_OID, univ.Null()) 141 | key = univ.Sequence().setComponentByPosition(0, univ.Integer(0)) # version 142 | for i in range(len(RSA_PARAMS)): 143 | key.setComponentByPosition(i+1, univ.Integer(params[RSA_PARAMS[i]])) 144 | octkey = encoder.encode(key) 145 | seq = ASN1Sequence(univ.Integer(0), oid, univ.OctetString(octkey)) 146 | return Encode(encoder.encode(seq)) 147 | 148 | def ExportDsaPkcs8(params): 149 | alg_params = univ.Sequence() 150 | for i in range(len(DSA_PARAMS)): 151 | alg_params.setComponentByPosition(i, univ.Integer(params[DSA_PARAMS[i]])) 152 | oid = ASN1Sequence(DSA_OID, alg_params) 153 | octkey = encoder.encode(univ.Integer(params['x'])) 154 | seq = ASN1Sequence(univ.Integer(0), oid, univ.OctetString(octkey)) 155 | return Encode(encoder.encode(seq)) 156 | 157 | #NOTE: not full X.509 certificate, just public key info 158 | #SubjectPublicKeyInfo ::= SEQUENCE { 159 | # algorithm AlgorithmIdentifier, 160 | # subjectPublicKey BIT STRING } 161 | def ParseX509(x509): 162 | seq = ParseASN1Sequence(decoder.decode(Decode(x509))[0]) 163 | if len(seq) != 2: # need two fields in SubjectPublicKeyInfo 164 | raise errors.KeyczarError("Illegal X.509 String.") 165 | [oid, alg_params] = ParseASN1Sequence(seq[0]) 166 | binstring = seq[1].prettyPrint()[1:-2] 167 | pubkey = decoder.decode(univ.OctetString(BinToBytes(binstring.replace("'", ""))))[0] 168 | # Component 1 should be a BIT STRING, get raw bits by discarding extra chars, 169 | # then convert to OCTET STRING which can be ASN.1 decoded 170 | params = {} 171 | if oid == RSA_OID: 172 | [params['n'], params['e']] = [long(x) for x in ParseASN1Sequence(pubkey)] 173 | elif oid == DSA_OID: 174 | vals = [long(x) for x in ParseASN1Sequence(alg_params)] 175 | for i in range(len(DSA_PARAMS)): 176 | params[DSA_PARAMS[i]] = vals[i] 177 | params['y'] = long(pubkey) 178 | else: 179 | raise errors.KeyczarError("Unrecognized AlgorithmIdentifier: not RSA/DSA") 180 | return params 181 | 182 | def ExportRsaX509(params): 183 | oid = ASN1Sequence(RSA_OID, univ.Null()) 184 | key = ASN1Sequence(univ.Integer(params['n']), univ.Integer(params['e'])) 185 | binkey = BytesToBin(encoder.encode(key)) 186 | pubkey = univ.BitString("'%s'B" % binkey) # needs to be a BIT STRING 187 | seq = ASN1Sequence(oid, pubkey) 188 | return Encode(encoder.encode(seq)) 189 | 190 | def ExportDsaX509(params): 191 | alg_params = ASN1Sequence(univ.Integer(params['p']), 192 | univ.Integer(params['q']), 193 | univ.Integer(params['g'])) 194 | oid = ASN1Sequence(DSA_OID, alg_params) 195 | binkey = BytesToBin(encoder.encode(univ.Integer(params['y']))) 196 | pubkey = univ.BitString("'%s'B" % binkey) # needs to be a BIT STRING 197 | seq = ASN1Sequence(oid, pubkey) 198 | return Encode(encoder.encode(seq)) 199 | 200 | def MakeDsaSig(r, s): 201 | """ 202 | Given the raw parameters of a DSA signature, return a Base64 signature. 203 | 204 | @param r: parameter r of DSA signature 205 | @type r: long int 206 | 207 | @param s: parameter s of DSA signature 208 | @type s: long int 209 | 210 | @return: raw byte string formatted as an ASN.1 sequence of r and s 211 | @rtype: string 212 | """ 213 | seq = ASN1Sequence(univ.Integer(r), univ.Integer(s)) 214 | return encoder.encode(seq) 215 | 216 | def ParseDsaSig(sig): 217 | """ 218 | Given a raw byte string, return tuple of DSA signature parameters. 219 | 220 | @param sig: byte string of ASN.1 representation 221 | @type sig: string 222 | 223 | @return: parameters r, s as a tuple 224 | @rtype: tuple 225 | 226 | @raise KeyczarErrror: if the DSA signature format is invalid 227 | """ 228 | seq = decoder.decode(sig)[0] 229 | if len(seq) != 2: 230 | raise errors.KeyczarError("Illegal DSA signature.") 231 | r = long(seq.getComponentByPosition(0)) 232 | s = long(seq.getComponentByPosition(1)) 233 | return (r, s) 234 | 235 | def MakeEmsaMessage(msg, modulus_size): 236 | """Algorithm EMSA_PKCS1-v1_5 from PKCS 1 version 2""" 237 | magic_sha1_header = [0x30, 0x21, 0x30, 0x9, 0x6, 0x5, 0x2b, 0xe, 0x3, 0x2, 238 | 0x1a, 0x5, 0x0, 0x4, 0x14] 239 | encoded = "".join([chr(c) for c in magic_sha1_header]) + Hash(msg) 240 | pad_string = chr(0xFF) * (modulus_size / 8 - len(encoded) - 3) 241 | return chr(1) + pad_string + chr(0) + encoded 242 | 243 | def BinToBytes(bits): 244 | """Convert bit string to byte string.""" 245 | bits = _PadByte(bits) 246 | octets = [bits[8*i:8*(i+1)] for i in range(len(bits)/8)] 247 | bytes = [chr(int(x, 2)) for x in octets] 248 | return "".join(bytes) 249 | 250 | def BytesToBin(bytes): 251 | """Convert byte string to bit string.""" 252 | return "".join([_PadByte(IntToBin(ord(byte))) for byte in bytes]) 253 | 254 | def _PadByte(bits): 255 | """Pad a string of bits with zeros to make its length a multiple of 8.""" 256 | r = len(bits) % 8 257 | return ((8-r) % 8)*'0' + bits 258 | 259 | def IntToBin(n): 260 | if n == 0 or n == 1: 261 | return str(n) 262 | elif n % 2 == 0: 263 | return IntToBin(n/2) + "0" 264 | else: 265 | return IntToBin(n/2) + "1" 266 | 267 | def BigIntToBytes(n): 268 | """Return a big-endian byte string representation of an arbitrary length n.""" 269 | chars = [] 270 | while (n > 0): 271 | chars.append(chr(n % 256)) 272 | n = n >> 8 273 | chars.reverse() 274 | return "".join(chars) 275 | 276 | def IntToBytes(n): 277 | """Return byte string of 4 big-endian ordered bytes representing n.""" 278 | bytes = [m % 256 for m in [n >> 24, n >> 16, n >> 8, n]] 279 | return "".join([chr(b) for b in bytes]) # byte array to byte string 280 | 281 | def BytesToLong(bytes): 282 | l = len(bytes) 283 | return long(sum([ord(bytes[i]) * 256**(l - 1 - i) for i in range(l)])) 284 | 285 | def Xor(a, b): 286 | """Return a ^ b as a byte string where a and b are byte strings.""" 287 | # pad shorter byte string with zeros to make length equal 288 | m = max(len(a), len(b)) 289 | if m > len(a): 290 | a = PadBytes(a, m - len(a)) 291 | elif m > len(b): 292 | b = PadBytes(b, m - len(b)) 293 | x = [ord(c) for c in a] 294 | y = [ord(c) for c in b] 295 | z = [chr(x[i] ^ y[i]) for i in range(m)] 296 | return "".join(z) 297 | 298 | def PadBytes(bytes, n): 299 | """Prepend a byte string with n zero bytes.""" 300 | return n * '\x00' + bytes 301 | 302 | def TrimBytes(bytes): 303 | """Trim leading zero bytes.""" 304 | trimmed = bytes.lstrip(chr(0)) 305 | if trimmed == "": # was a string of all zero bytes 306 | return chr(0) 307 | else: 308 | return trimmed 309 | 310 | def RandBytes(n): 311 | """Return n random bytes.""" 312 | # This function requires at least Python 2.4. 313 | return os.urandom(n) 314 | 315 | def Hash(*inputs): 316 | """Return a SHA-1 hash over a variable number of inputs.""" 317 | md = sha1() 318 | for i in inputs: 319 | md.update(i) 320 | return md.digest() 321 | 322 | def PrefixHash(*inputs): 323 | """Return a SHA-1 hash over a variable number of inputs.""" 324 | md = sha1() 325 | for i in inputs: 326 | md.update(IntToBytes(len(i))) 327 | md.update(i) 328 | return md.digest() 329 | 330 | 331 | def Encode(s): 332 | """ 333 | Return Base64 encoding of s. 334 | 335 | @param s: string to encode as Base64 336 | @type s: string 337 | 338 | @return: Base64 representation of s. 339 | @rtype: string 340 | """ 341 | return base64.b64encode(str(s)) 342 | 343 | 344 | def Decode(s): 345 | """ 346 | Return decoded version of given Base64 string. Ignore whitespace. 347 | 348 | @param s: Base64 string to decode 349 | @type s: string 350 | 351 | @return: original string that was encoded as Base64 352 | @rtype: string 353 | 354 | @raise Base64DecodingError: If length of string (ignoring whitespace) is one 355 | more than a multiple of four. 356 | """ 357 | s = str(s.replace(" ", "")) # kill whitespace, make string (not unicode) 358 | d = len(s) % 4 359 | if d == 1: 360 | raise errors.Base64DecodingError() 361 | elif d == 2: 362 | s += "==" 363 | elif d == 3: 364 | s += "=" 365 | return base64.b64decode(s) 366 | 367 | def WriteFile(data, loc): 368 | """ 369 | Writes data to file at given location. 370 | 371 | @param data: contents to be written to file 372 | @type data: string 373 | 374 | @param loc: name of file to write to 375 | @type loc: string 376 | 377 | @raise KeyczarError: if unable to write to file because of IOError 378 | """ 379 | try: 380 | f = open(loc, "w") 381 | f.write(data) 382 | f.close() 383 | except IOError: 384 | raise errors.KeyczarError("Unable to write to file %s." % loc) 385 | 386 | def ReadFile(loc): 387 | """ 388 | Read data from file at given location. 389 | 390 | @param loc: name of file to read from 391 | @type loc: string 392 | 393 | @return: contents of the file 394 | @rtype: string 395 | 396 | @raise KeyczarError: if unable to read from file because of IOError 397 | """ 398 | try: 399 | return open(loc).read() 400 | except IOError: 401 | raise errors.KeyczarError("Unable to read file %s." % loc) 402 | 403 | def MGF(seed, mlen): 404 | """ 405 | Mask Generation Function (MGF1) with SHA-1 as hash. 406 | 407 | @param seed: used to generate mask, a byte string 408 | @type seed: string 409 | 410 | @param mlen: desired length of mask 411 | @type mlen: integer 412 | 413 | @return: mask, byte string of length mlen 414 | @rtype: string 415 | 416 | @raise KeyczarError: if mask length too long, > 2^32 * hash_length 417 | """ 418 | if mlen > 2**32 * HLEN: 419 | raise errors.KeyczarError("MGF1 mask length too long.") 420 | output = "" 421 | for i in range(int(math.ceil(mlen / float(HLEN)))): 422 | output += Hash(seed, IntToBytes(i)) 423 | return output[:mlen] 424 | 425 | 426 | def fingerprint(key): 427 | '''generate the human readable form of the fingerprint as used in OTR''' 428 | return '{0:040x}'.format(bytes_to_long(DSAKey(key).fingerprint())) 429 | 430 | 431 | def check_and_set(key, k, v): 432 | ''' 433 | Check if a key is already in the keydict, check its contents against the 434 | supplied value. If the key does not exist, then create a new entry in 435 | keydict. If the key exists and has a different value, throw an exception. 436 | ''' 437 | if k in key: 438 | if key[k] != v: 439 | if 'name' in key: 440 | name = key['name'] 441 | else: 442 | name = '(unknown)' 443 | # this should be an Exception so that the GUI can catch it to handle it 444 | print('"' + k + '" values for "' + name + '" did not match: \n\t"' + str(key[k]) 445 | + '" != "' + str(v) + '"') 446 | else: 447 | key[k] = v 448 | 449 | def merge_keys(key1, key2): 450 | '''merge the second key data into the first, checking for conflicts''' 451 | for k, v in key2.items(): 452 | check_and_set(key1, k, v) 453 | 454 | 455 | def merge_keydicts(kd1, kd2): 456 | ''' 457 | given two keydicts, merge the second one into the first one and report errors 458 | ''' 459 | for name, key in kd2.items(): 460 | if name in kd1: 461 | merge_keys(kd1[name], key) 462 | else: 463 | kd1[name] = key 464 | 465 | 466 | def _get_pids(): 467 | '''python-psutil's API changed in v3.0''' 468 | try: 469 | return psutil.pids() # v3.0+ 470 | except AttributeError: 471 | return psutil.get_pid_list() # <= 2.2.1 472 | 473 | 474 | def which_apps_are_running(apps): 475 | ''' 476 | Check the process list to see if any of the specified apps are running. 477 | It returns a tuple of running apps. 478 | ''' 479 | running = [] 480 | for pid in _get_pids(): 481 | try: 482 | p = psutil.Process(pid) 483 | except Exception as e: 484 | print(e) 485 | continue 486 | for app in apps: 487 | if app == p.name: 488 | running.append(app) 489 | print('found: ' + p.name) 490 | else: 491 | r = re.compile('.*' + app + '.*', re.IGNORECASE) 492 | try: 493 | for arg in p.cmdline: 494 | m = r.match(arg) 495 | if m and (p.name == 'python' or p.name == 'java'): 496 | running.append(app) 497 | break 498 | except: 499 | pass 500 | return tuple(running) 501 | 502 | 503 | def killall(app): 504 | ''' 505 | terminates all instances of an app 506 | ''' 507 | for pid in _get_pids(): 508 | p = psutil.Process(pid) 509 | if app == p.name: 510 | print('killing: ' + p.name) 511 | os.kill(pid, signal.SIGKILL) 512 | else: 513 | r = re.compile('.*' + app + '.*', re.IGNORECASE) 514 | try: 515 | for arg in p.cmdline: 516 | m = r.match(arg) 517 | if m and (p.name == 'python' or p.name == 'java'): 518 | print('killing: ' + p.name) 519 | os.kill(pid, signal.SIGKILL) 520 | break 521 | except: 522 | pass 523 | 524 | 525 | def _fullcopy(src, dst): 526 | ''' 527 | A simple full file copy that ignores perms, since MTP doesn't play well 528 | with them. shutil.copy() tries to dup perms... 529 | ''' 530 | with open(src) as f: 531 | content = f.readlines() 532 | with open(dst, 'w') as f: 533 | f.writelines(content) 534 | 535 | 536 | def make_conffile_backup(filename): 537 | '''makes a in-place backup of the given config file name''' 538 | realpath = os.path.realpath(filename) # eliminate symlinks 539 | s = os.stat(realpath) 540 | timestamp = s.st_mtime 541 | _fullcopy(realpath, realpath + '.' + str(timestamp)) 542 | 543 | 544 | def find_gvfs_destdir(): 545 | '''find the MTP subfolder in gvfs to copy the keystore to''' 546 | if os.path.exists(mtp.gvfs_mountpoint): 547 | foundit = False 548 | # this assumes that gvfs is mounting the MTP device 549 | if os.path.isdir(os.path.join(mtp.gvfs_mountpoint, 'Internal storage')): 550 | mtpdir = os.path.join(mtp.gvfs_mountpoint, 'Internal storage') 551 | foundit = True 552 | elif os.path.isdir(os.path.join(mtp.gvfs_mountpoint, 'SD card')): 553 | mtpdir = os.path.join(mtp.gvfs_mountpoint, 'SD card') 554 | foundit = True 555 | else: 556 | # if no standard names, try the first dir we find 557 | files = os.listdir(mtp.gvfs_mountpoint) 558 | if len(files) > 0: 559 | for f in files: 560 | fp = os.path.join(mtp.gvfs_mountpoint, f) 561 | if os.path.isdir(fp): 562 | mtpdir = fp 563 | foundit = True 564 | if foundit: 565 | return mtpdir 566 | 567 | 568 | def can_sync_to_device(): 569 | '''checks if an MTP device is mounted, i.e. an Android 4.x device''' 570 | mtp.devicename = '' 571 | if sys.platform == 'win32': 572 | # Right now the win32 'sync' method is to prompt the user to manually 573 | # copy the file over, so we always return true. 574 | # https://dev.guardianproject.info/issues/2126 575 | mtp.devicename = 'Copy the otr_keystore.ofcaes file to your device!' 576 | return True 577 | 578 | gvfs_destdir = find_gvfs_destdir() 579 | if gvfs_destdir and os.path.exists(gvfs_destdir): 580 | # this assumes that gvfs is mounting the MTP device. gvfs is 581 | # part of GNOME, but is probably included in other systems too 582 | mtp.devicename = gvfs_destdir 583 | return True 584 | 585 | # if all else fails, try pymtp. works on GNU/Linux and Mac OS X at least 586 | try: 587 | devices = mtp.detect_devices() 588 | if len(devices) > 0: 589 | e = devices[0].device_entry 590 | mtp.devicename = e.vendor + ' ' + e.product 591 | return True 592 | else: 593 | return False 594 | except Exception as e: 595 | print('except ' + str(e)) 596 | return False 597 | 598 | 599 | def get_keystore_savedir(): 600 | '''get a temp place to write out the encrypted keystore''' 601 | # first cache the encrypted file store in a local temp dir, then we can 602 | # separately handle copying it via MTP, gvfs, wmdlib, KIO, etc. 603 | return tempfile.mkdtemp(prefix='.keysync-') 604 | 605 | 606 | def sync_file_to_device(filename): 607 | '''sync the keystore file to the device via whatever the relevant method is''' 608 | 609 | 610 | #------------------------------------------------------------------------------# 611 | # for testing from the command line: 612 | def main(argv): 613 | import pprint 614 | 615 | key = dict() 616 | key['name'] = 'key' 617 | key['test'] = 'yes, testing' 618 | try: 619 | check_and_set(key, 'test', 'no, this should break') 620 | except Exception as e: 621 | print('Exception: ', end=' ') 622 | print(e) 623 | print(key['test']) 624 | check_and_set(key, 'test', 'yes, testing') 625 | print(key['test']) 626 | check_and_set(key, 'new', 'this is a new value') 627 | 628 | key2 = dict() 629 | key2['name'] = 'key' 630 | key2['yat'] = 'yet another test' 631 | merge_keys(key, key2) 632 | print('key: ', end=' ') 633 | pprint.pprint(key) 634 | 635 | # now make trouble again 636 | key2['test'] = 'yet another breakage' 637 | try: 638 | merge_keys(key, key2) 639 | except Exception as e: 640 | print('Exception: ', end=' ') 641 | print(e) 642 | 643 | # now let's try dicts of dicts aka 'keydict' 644 | keydict = dict() 645 | keydict['key'] = key 646 | key3 = dict() 647 | key3['name'] = 'key' 648 | key3['protocol'] = 'prpl-jabber' 649 | key4 = dict() 650 | key4['name'] = 'key4' 651 | key4['protocol'] = 'prpl-jabber' 652 | key4['fingerprint'] = 'gotone' 653 | key4['teststate'] = 'this one should not be merged' 654 | key5 = dict() 655 | key5['name'] = 'key' 656 | key5['protocol'] = 'prpl-jabber' 657 | key5['moreinfo'] = 'even more!' 658 | keydict2 = dict() 659 | keydict2['key'] = key3 660 | keydict2['key'] = key5 661 | keydict2['key4'] = key4 662 | merge_keydicts(keydict, keydict2) 663 | pprint.pprint(keydict) 664 | 665 | # one more test 666 | print('---------------------------') 667 | key6 = dict() 668 | key6['name'] = 'key' 669 | keydict3 = dict() 670 | keydict3['key'] = key6 671 | pprint.pprint(keydict3['key']) 672 | merge_keys(keydict3['key'], key3) 673 | pprint.pprint(keydict3['key']) 674 | merge_keys(keydict3['key'], key5) 675 | pprint.pprint(keydict3['key']) 676 | 677 | sys.path.insert(0, os.path.abspath('..')) 678 | import otrapps 679 | print('\n---------------------------') 680 | print('Which supported apps are currently running:') 681 | print((which_apps_are_running(otrapps.__all__))) 682 | 683 | print('\n---------------------------') 684 | print('make backup conf file: ') 685 | tmpdir = tempfile.mkdtemp(prefix='.keysync-util-test-') 686 | testfile = os.path.join(tmpdir, 'keysync-util-conffile-backup-test') 687 | with open(testfile, 'w') as f: 688 | f.write('this is just a test!\n') 689 | make_conffile_backup(testfile) 690 | print('Backed up "%s"' % testfile) 691 | 692 | if can_sync_to_device(): 693 | print('\n---------------------------') 694 | print('MTP is mounted here:', end=' ') 695 | print(mtp.devicename) 696 | 697 | 698 | 699 | if __name__ == "__main__": 700 | main(sys.argv[1:]) 701 | -------------------------------------------------------------------------------- /otrapps/xchat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''a module for reading and writing Xchat's OTR key data''' 4 | 5 | from __future__ import print_function 6 | import os 7 | import sys 8 | 9 | if __name__ == '__main__': 10 | sys.path.insert(0, "../") # so the main() test suite can find otrapps module 11 | import otrapps.util 12 | from otrapps.otr_private_key import OtrPrivateKeys 13 | from otrapps.otr_fingerprints import OtrFingerprints 14 | 15 | class XchatProperties(): 16 | 17 | path = os.path.expanduser('~/.xchat2/otr') 18 | keyfile = 'otr.key' 19 | fingerprintfile = 'otr.fp' 20 | files = (keyfile, fingerprintfile) 21 | 22 | @staticmethod 23 | def parse(settingsdir=None): 24 | if settingsdir == None: 25 | settingsdir = XchatProperties.path 26 | 27 | kf = os.path.join(settingsdir, XchatProperties.keyfile) 28 | if os.path.exists(kf): 29 | keydict = OtrPrivateKeys.parse(kf) 30 | else: 31 | keydict = dict() 32 | 33 | fpf = os.path.join(settingsdir, XchatProperties.fingerprintfile) 34 | if os.path.exists(fpf): 35 | otrapps.util.merge_keydicts(keydict, OtrFingerprints.parse(fpf)) 36 | 37 | return keydict 38 | 39 | @staticmethod 40 | def write(keydict, savedir): 41 | if not os.path.exists(savedir): 42 | raise Exception('"' + savedir + '" does not exist!') 43 | 44 | kf = os.path.join(savedir, XchatProperties.keyfile) 45 | OtrPrivateKeys.write(keydict, kf) 46 | 47 | accounts = [] 48 | # look for all private keys and use them for the accounts list 49 | for name, key in keydict.items(): 50 | if 'x' in key: 51 | accounts.append(name) 52 | fpf = os.path.join(savedir, XchatProperties.fingerprintfile) 53 | OtrFingerprints.write(keydict, fpf, accounts) 54 | 55 | 56 | 57 | if __name__ == '__main__': 58 | 59 | import pprint 60 | 61 | print('Xchat stores its files in ' + XchatProperties.path) 62 | 63 | if len(sys.argv) == 2: 64 | settingsdir = sys.argv[1] 65 | else: 66 | settingsdir = '../tests/xchat' 67 | 68 | keydict = XchatProperties.parse(settingsdir) 69 | pprint.pprint(keydict) 70 | 71 | XchatProperties.write(keydict, '/tmp') 72 | -------------------------------------------------------------------------------- /py2app-python2.6.patch: -------------------------------------------------------------------------------- 1 | # HG changeset patch 2 | # User Ronald Oussoren 3 | # Date 1362420069 -3600 4 | # Branch branch-0.7 5 | # Node ID 70438e65f35b38fff433c883942732bfbe7bb14b 6 | # Parent da87652fa58b23646d0ab08b46b0faac88315d0f 7 | Fixes python 2.6 breakage 8 | 9 | fixes #96 10 | 11 | diff --git a/py2app/build_app.py b/py2app/build_app.py 12 | --- a/py2app/build_app.py 13 | +++ b/py2app/build_app.py 14 | @@ -18,12 +18,6 @@ 15 | from py2app.apptemplate.setup import main as script_executable 16 | from py2app.util import mergecopy, make_exec 17 | 18 | - 19 | -try: 20 | - import sysconfig 21 | -except ImportError: 22 | - sysconfig = None 23 | - 24 | try: 25 | from cStringIO import StringIO 26 | except ImportError: 27 | @@ -499,8 +493,8 @@ 28 | yield runtime 29 | 30 | def run(self): 31 | - if sysconfig.get_config_var('PYTHONFRAMEWORK') is None: 32 | - if not sysconfig.get_config_var('Py_ENABLE_SHARED'): 33 | + if get_config_var('PYTHONFRAMEWORK') is None: 34 | + if not get_config_var('Py_ENABLE_SHARED'): 35 | raise DistutilsPlatformError("This python does not have a shared library or framework") 36 | 37 | else: 38 | @@ -1132,11 +1126,8 @@ 39 | # XXX - In this particular case we know exactly what we can 40 | # get away with.. should this be extended to the general 41 | # case? Per-framework recipes? 42 | - includedir = None 43 | - configdir = None 44 | - if sysconfig is not None: 45 | - includedir = sysconfig.get_config_var('CONFINCLUDEPY') 46 | - configdir = sysconfig.get_config_var('LIBPL') 47 | + includedir = get_config_var('CONFINCLUDEPY') 48 | + configdir = get_config_var('LIBPL') 49 | 50 | 51 | if includedir is None: 52 | @@ -1516,9 +1507,8 @@ 53 | 54 | includedir = None 55 | configdir = None 56 | - if sysconfig is not None: 57 | - includedir = sysconfig.get_config_var('CONFINCLUDEPY') 58 | - configdir = sysconfig.get_config_var('LIBPL') 59 | + includedir = get_config_var('CONFINCLUDEPY') 60 | + configdir = get_config_var('LIBPL') 61 | 62 | 63 | if includedir is None: 64 | @@ -1582,11 +1572,8 @@ 65 | os.unlink(site_path) 66 | 67 | 68 | - includedir = None 69 | - configdir = None 70 | - if sysconfig is not None: 71 | - includedir = sysconfig.get_config_var('CONFINCLUDEPY') 72 | - configdir = sysconfig.get_config_var('LIBPL') 73 | + includedir = get_config_var('CONFINCLUDEPY') 74 | + configdir = get_config_var('LIBPL') 75 | 76 | 77 | if includedir is None: 78 | @@ -1650,11 +1637,8 @@ 79 | os.unlink(site_path) 80 | 81 | 82 | - includedir = None 83 | - configdir = None 84 | - if sysconfig is not None: 85 | - includedir = sysconfig.get_config_var('CONFINCLUDEPY') 86 | - configdir = sysconfig.get_config_var('LIBPL') 87 | + includedir = get_config_var('CONFINCLUDEPY') 88 | + configdir = get_config_var('LIBPL') 89 | 90 | 91 | if includedir is None: 92 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | import os 6 | import sys 7 | 8 | def read(fname): 9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 10 | 11 | dependencies = [ 12 | 'BeautifulSoup4', 13 | 'psutil', 14 | 'python-potr', 15 | 'pyasn1', 16 | 'pycrypto', 17 | 'pyjavaproperties', 18 | 'pyparsing', 19 | 'pgpdump', 20 | 'qrcode >= 4.0.1', 21 | 'six', 22 | ] 23 | 24 | # argparse and ordereddict are included in Python starting in 2.7 25 | if sys.version_info[0] == 2 and sys.version_info[1] < 7: 26 | dependencies.append('argparse') 27 | dependencies.append('ordereddict') 28 | 29 | if sys.platform == 'darwin': 30 | dependencies.append('PIL') 31 | dependencies.append('pymtp>=0.0.6') 32 | extra_options = dict( 33 | setup_requires=['py2app'], 34 | app=['keysync-gui'], 35 | # Cross-platform applications generally expect sys.argv to 36 | # be used for opening files. 37 | options=dict( 38 | py2app=dict( 39 | argv_emulation=True, 40 | semi_standalone=False, 41 | use_pythonpath=False, 42 | site_packages=True, 43 | frameworks='/sw/lib/libmtp.9.dylib', 44 | iconfile='icons/keysync.icns', 45 | plist={ 46 | 'CFBundleIdentifier': 'info.guardianproject.keysync', 47 | 'CFBundleName': 'KeySync', 48 | 'CFBundleLocalizations': ['en'], 49 | 'PyRuntimeLocations': ['/System/Library/Frameworks/Python.framework/Versions/2.6/Python'], 50 | }, 51 | ), 52 | ), 53 | ) 54 | elif sys.platform == 'win32': 55 | dependencies.append('pyinstaller') 56 | dependencies.append('pywin32') 57 | # PIL doesn't build on Windows, so use Pillow instead, pegged at 58 | # 2.1.0 until pyinstaller supports newer version 59 | dependencies.append('Pillow==2.1.0') 60 | extra_options = dict() 61 | else: 62 | dependencies.append('Pillow>=2.1.0') 63 | dependencies.append('pymtp>=0.0.6') 64 | extra_options = dict() 65 | 66 | 67 | setup(name='keysync', 68 | version='0.2.2', 69 | description='syncs OTR keys between different IM programs', 70 | long_description=read('README.md'), 71 | author='The Guardian Project', 72 | author_email='support@guardianproject.info', 73 | url='https://guardianproject.info/apps/keysync', 74 | packages=['otrapps'], 75 | scripts=['keysync', 'keysync-gui'], 76 | data_files=[ 77 | ('share/man/man1', ['man/keysync.1']), 78 | ('share/icons/hicolor/128x128/apps', ['icons/128x128/keysync.png']), 79 | ('share/icons/hicolor/256x256/apps', ['icons/keysync.png']), 80 | ('share/keysync', 81 | ['icons/add.png', 'icons/adium.png', 'icons/chatsecure.png', 82 | 'icons/gajim.png', 'icons/gnupg.png', 'icons/irssi.png', 83 | 'icons/jitsi.png', 'icons/keysync.png', 'icons/kopete.png', 84 | 'icons/pidgin.png', 'icons/xchat.png']), 85 | ('share/applications', ['keysync.desktop']) 86 | ], 87 | license='GPLv3+', 88 | classifiers=[ 89 | 'Development Status :: 3 - Alpha', 90 | 'Intended Audience :: End Users/Desktop', 91 | 'Intended Audience :: Developers', 92 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 93 | 'Operating System :: POSIX', 94 | 'Operating System :: MacOS :: MacOS X', 95 | 'Operating System :: Microsoft :: Windows', 96 | 'Topic :: Communications :: Chat', 97 | 'Topic :: Software Development :: Libraries', 98 | 'Topic :: Software Development :: Libraries :: Python Modules', 99 | 'Topic :: Utilities', 100 | ], 101 | install_requires=dependencies, 102 | **extra_options 103 | ) 104 | -------------------------------------------------------------------------------- /tests/SAMENESS/adium/Accounts.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Accounts 6 | 7 | 8 | ObjectID 9 | 1 10 | Service 11 | Jabber 12 | Type 13 | libpurple-Jabber 14 | UID 15 | gptest@limun.org 16 | 17 | 18 | TopAccountID 19 | 2 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/SAMENESS/adium/otr.fingerprints: -------------------------------------------------------------------------------- 1 | fakename@jabber.ccc.de 1 libpurple-Jabber c298712572dfeead3e7624840f9120916da7f8a8 verified 2 | gptest@jabber.ccc.de 1 libpurple-Jabber a2cd015572d256703e7624840f91209160185410 verified 3 | gptest@jabber.org 1 libpurple-jabber 39e90112c19d45cecb968ff981d32e9c9a5fe284 4 | gptest@jabber.org 1 libpurple-Jabber f83fe8c909c4deb51c234574bab3e4f66af215f8 verified 5 | gptest@limun.org 1 libpurple-Jabber 39e90112c19d45cecb968ff981d32e9c9a5fe284 6 | gptest@slipjack.com 1 libpurple-Jabber 8d0f48cb59348f3aa596dc7b1f5048c14368cf5d verified 7 | hans@eds.org 1 libpurple-Jabber 2f66e8c909c4deb51cbd4a02b9e6af4d6af215f8 verified 8 | hans@eds.org 1 libpurple-Jabber f83fe8c909c4deb51c234574bab3e4f66af215f8 9 | n8fr8@jabber.com 1 libpurple-Jabber 92967e4743bc730e0a3396be1a6ea365cc38f0f3 10 | -------------------------------------------------------------------------------- /tests/SAMENESS/adium/otr.private_key: -------------------------------------------------------------------------------- 1 | (privkeys 2 | (account 3 | (name "1") 4 | (protocol libpurple-Jabber) 5 | (private-key 6 | (dsa 7 | (p #008D3EE66820824BDB81FCFEE853A2148C600D22C4D5D66ED7535A2F1F890A130239AFC3619FE2B5886EE2BEE109170D8D56B79AEB2217D490F92AA5A82F1B562CB19E51F3C38C717B4A0AA0306A9CBDF62977566AE041F3E2D60CE02C82ED9F350A38A016E9CB3C93C4E5F9A8B14ED738E9CB0D076EEB02009D0CBBF7AF36370B#) 8 | (q #00A7C84E6CB16B190F2FC962EDA3D77926490F902F#) 9 | (g #707219B8AE0783C234DF2426AA6E69F30738AC112FB0B53CC54776959AAF67626BB895E18AA8B735DD3E19BE780B5B7193DB2D64159B4757B0CB23AC555B70F25AF1474FE4D9C44059F8BFCA1BEBC845C4696A558ABA0C916368D3961EEBB8130E4651B618384E687CF7A9917637D97BE51D44217937F7D8A9A2C9ED850B0BAC#) 10 | (y #329D74BDCA8399977150519EDBC018988D43393834F77E36D577772B9434B2EE08B572BDBB7F4A23F5242FFCDBD90A4A8D405C8366CAB785455B431B888DB412BC37713648D6F5C919E501E949BA8B719BE3EF31B6244D483BCCAE042C1E6E6DF462E5A5064BED031A55BC1E3DAAF38895A1B92A20C6D82D78F90C24CABBC566#) 11 | (x #7A477B8980503487CEE95628EBAC52A19274BD57#) 12 | ) 13 | ) 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /tests/SAMENESS/pidgin/otr.fingerprints: -------------------------------------------------------------------------------- 1 | fakename@jabber.ccc.de gptest@limun.org/ prpl-jabber c298712572dfeead3e7624840f9120916da7f8a8 verified 2 | gptest@jabber.ccc.de gptest@limun.org/ prpl-jabber a2cd015572d256703e7624840f91209160185410 verified 3 | gptest@jabber.org gptest@limun.org/ prpl-jabber 39e90112c19d45cecb968ff981d32e9c9a5fe284 4 | gptest@jabber.org gptest@limun.org/ prpl-jabber f83fe8c909c4deb51c234574bab3e4f66af215f8 verified 5 | gptest@limun.org gptest@limun.org/ prpl-jabber 39e90112c19d45cecb968ff981d32e9c9a5fe284 6 | gptest@slipjack.com gptest@limun.org/ prpl-jabber 8d0f48cb59348f3aa596dc7b1f5048c14368cf5d verified 7 | hans@eds.org gptest@limun.org/ prpl-jabber 2f66e8c909c4deb51cbd4a02b9e6af4d6af215f8 verified 8 | hans@eds.org gptest@limun.org/ prpl-jabber f83fe8c909c4deb51c234574bab3e4f66af215f8 9 | n8fr8@jabber.com gptest@limun.org/ prpl-jabber 92967e4743bc730e0a3396be1a6ea365cc38f0f3 10 | -------------------------------------------------------------------------------- /tests/SAMENESS/pidgin/otr.private_key: -------------------------------------------------------------------------------- 1 | (privkeys 2 | (account 3 | (name "gptest@limun.org/") 4 | (protocol prpl-jabber) 5 | (private-key 6 | (dsa 7 | (p #008D3EE66820824BDB81FCFEE853A2148C600D22C4D5D66ED7535A2F1F890A130239AFC3619FE2B5886EE2BEE109170D8D56B79AEB2217D490F92AA5A82F1B562CB19E51F3C38C717B4A0AA0306A9CBDF62977566AE041F3E2D60CE02C82ED9F350A38A016E9CB3C93C4E5F9A8B14ED738E9CB0D076EEB02009D0CBBF7AF36370B#) 8 | (q #00A7C84E6CB16B190F2FC962EDA3D77926490F902F#) 9 | (g #707219B8AE0783C234DF2426AA6E69F30738AC112FB0B53CC54776959AAF67626BB895E18AA8B735DD3E19BE780B5B7193DB2D64159B4757B0CB23AC555B70F25AF1474FE4D9C44059F8BFCA1BEBC845C4696A558ABA0C916368D3961EEBB8130E4651B618384E687CF7A9917637D97BE51D44217937F7D8A9A2C9ED850B0BAC#) 10 | (y #329D74BDCA8399977150519EDBC018988D43393834F77E36D577772B9434B2EE08B572BDBB7F4A23F5242FFCDBD90A4A8D405C8366CAB785455B431B888DB412BC37713648D6F5C919E501E949BA8B719BE3EF31B6244D483BCCAE042C1E6E6DF462E5A5064BED031A55BC1E3DAAF38895A1B92A20C6D82D78F90C24CABBC566#) 11 | (x #7A477B8980503487CEE95628EBAC52A19274BD57#) 12 | ) 13 | ) 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /tests/adium/Accounts.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Accounts 6 | 7 | 8 | ObjectID 9 | 9 10 | Service 11 | Bonjour 12 | Type 13 | bonjour-libezv 14 | UID 15 | GPTest 16 | 17 | 18 | ObjectID 19 | 1 20 | Service 21 | Jabber 22 | Type 23 | libpurple-Jabber 24 | UID 25 | gptest@jabber.org 26 | 27 | 28 | ObjectID 29 | 2 30 | Service 31 | GTalk 32 | Type 33 | libpurple-jabber-gtalk 34 | UID 35 | gptest@slipjack.com 36 | 37 | 38 | TopAccountID 39 | 10 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Login Preferences.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Login Preferences.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/AccountPrefs.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/AccountPrefs.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/Accounts.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Accounts 6 | 7 | 8 | ObjectID 9 | 1 10 | Service 11 | Jabber 12 | Type 13 | libpurple-Jabber 14 | UID 15 | gptest@jabber.org 16 | 17 | 18 | ObjectID 19 | 2 20 | Service 21 | GTalk 22 | Type 23 | libpurple-jabber-gtalk 24 | UID 25 | gptest@slipjack.com 26 | 27 | 28 | TopAccountID 29 | 3 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/ByObjectPrefs.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/ByObjectPrefs.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/Contact Alerts.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/Contact Alerts.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/Contact List.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/Contact List.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/Event Presets.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/Event Presets.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/General.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/General.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/List Layout.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/List Layout.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/List Theme.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/List Theme.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/Logging.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/Logging.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/OTR.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/OTR.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/URL Handling Group.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/URL Handling Group.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/WebKit Message Display.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/adium/Library/Application Support/Adium 2.0/Users/Default/WebKit Message Display.plist -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/libpurple/accounts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | prpl-jabber 6 | gptest@jabber.org/palatschinken-3 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 | <vCard xmlns='vcard-temp'/> 33 | 34 | 0 35 | 36 | 0 37 | 38 | 39 | 0 40 | proxy.eu.jabber.org 41 | 1 42 | jabber.org 43 | 5222 44 | opportunistic_tls 45 | 46 | 47 | 1 48 | 49 | 50 | 51 | 52 | prpl-jabber 53 | gptest@slipjack.com/Adium 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 | <vCard xmlns='vcard-temp'><FN>Guardian Project Test</FN></vCard> 80 | 81 | 0 82 | 83 | 0 84 | talk.google.com 85 | 86 | 0 87 | proxy.eu.jabber.org 88 | 1 89 | talk.google.com 90 | 5222 91 | opportunistic_tls 92 | 93 | 94 | 1 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/libpurple/blist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | gptest@slipjack.com 9 | 10 | 11 | 12 | 13 | hans@eds.org 14 | hans@eds.org 15 | 14c6c6f5962a46bebb7f866af4f246bc899f954c 16 | 14c6c6f5962a46bebb7f866af4f246bc899f954c.png 17 | 18 | 19 | 20 | 21 | hans@eds.org 22 | 14c6c6f5962a46bebb7f866af4f246bc899f954c 23 | 14c6c6f5962a46bebb7f866af4f246bc899f954c.png 24 | 25 | 26 | 27 | 28 | update@identi.ca 29 | update 30 | 31 | 32 | 33 | 34 | walker@slipjack.com 35 | walker 36 | 6fb356c6b00475b847823999b6269d16cc56ae89 37 | 6fb356c6b00475b847823999b6269d16cc56ae89.png 38 | 39 | 40 | 41 | 42 | gptest@jabber.org 43 | 44 | 45 | 46 | 47 | pdgsocmentor@gmail.com 48 | 49 | 50 | 51 | 52 | 53 | 54 | guardian.djh@gmail.com 55 | guardian.djh@gmail.com 56 | 57 | 58 | 59 | 60 | gptest@jabber.ccc.de 61 | gptest@jabber.ccc.de 62 | 63 | 64 | 65 | 66 | gp.test@jabber.org 67 | gp.test@jabber.org 68 | 69 | 70 | 71 | 72 | pdgsocmentor@gmail.com 73 | pdgsocmentor@gmail.com 74 | 75 | 76 | 77 | 78 | guardiantest@jabber.org 79 | guardiantest@jabber.org 80 | 81 | 82 | 83 | 84 | gptestlaptop@jabber.org 85 | gptestlaptop@jabber.org 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/libpurple/prefs.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /tests/adium/Library/Application Support/Adium 2.0/Users/Default/libpurple/xmpp-caps.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /tests/adium/otr.fingerprints: -------------------------------------------------------------------------------- 1 | gptest@jabber.org 2 libpurple-jabber-gtalk 39e90112c19d45cecb968ff981d32e9c9a5fe284 verified 2 | gptest@jabber.org 1 libpurple-Jabber f83fe8c909c4deb51c234574bab3e4f66af215f8 verified 3 | gptest@slipjack.com 1 libpurple-Jabber 8d0f48cb59348f3aa596dc7b1f5048c14368cf5d verified 4 | hans@eds.org 1 libpurple-Jabber ff66e8c909c4deb51cbd4a02b9e6af4d6af215f8 verified 5 | -------------------------------------------------------------------------------- /tests/adium/otr.private_key: -------------------------------------------------------------------------------- 1 | (privkeys 2 | (account 3 | (name "1") 4 | (protocol libpurple-Jabber) 5 | (private-key 6 | (dsa 7 | (p #008D3EE66820824BDB81FCFEE853A2148C600D22C4D5D66ED7535A2F1F890A130239AFC3619FE2B5886EE2BEE109170D8D56B79AEB2217D490F92AA5A82F1B562CB19E51F3C38C717B4A0AA0306A9CBDF62977566AE041F3E2D60CE02C82ED9F350A38A016E9CB3C93C4E5F9A8B14ED738E9CB0D076EEB02009D0CBBF7AF36370B#) 8 | (q #00A7C84E6CB16B190F2FC962EDA3D77926490F902F#) 9 | (g #707219B8AE0783C234DF2426AA6E69F30738AC112FB0B53CC54776959AAF67626BB895E18AA8B735DD3E19BE780B5B7193DB2D64159B4757B0CB23AC555B70F25AF1474FE4D9C44059F8BFCA1BEBC845C4696A558ABA0C916368D3961EEBB8130E4651B618384E687CF7A9917637D97BE51D44217937F7D8A9A2C9ED850B0BAC#) 10 | (y #329D74BDCA8399977150519EDBC018988D43393834F77E36D577772B9434B2EE08B572BDBB7F4A23F5242FFCDBD90A4A8D405C8366CAB785455B431B888DB412BC37713648D6F5C919E501E949BA8B719BE3EF31B6244D483BCCAE042C1E6E6DF462E5A5064BED031A55BC1E3DAAF38895A1B92A20C6D82D78F90C24CABBC566#) 11 | (x #7A477B8980503487CEE95628EBAC52A19274BD57#) 12 | ) 13 | ) 14 | ) 15 | (account 16 | (name "2") 17 | (protocol libpurple-jabber-gtalk) 18 | (private-key 19 | (dsa 20 | (p #00F24843F9447B62138AE49BF83188D1353ADA5CAC118890CFDEC01BF349D75E887B19C221665C7857CAD583AF656C67FB04A99FD8F8D69D09C9529C6C14D426F1E3924DC9243AF2970E3E4B04A23489A09E8A90E7E81EBA763AD4F0636B8A43415B6FC16A02C3624CE76272FA00783C8DB850D3A996B58136F7A0EB80AE0BC613#) 21 | (q #00D16B2607FCBC0EDC639F763A54F34475B1CC8473#) 22 | (g #00B15AFEF5F96EFEE41006F136C23A18849DA8133069A879D083F7C7AA362E187DAE3ED0C4F372D0D4E3AAE567008A1872A6E85D8F84E53A3FE1B352AF0B4E2F0CB033A6D34285ECD3E4A93653BDE99C3A8D840D9D35F82AC2FA8539DB6C7F7A1DAD77FEECD62803757FF1E2DE4CEC4A5A2AD643271514DDEEEF3D008F66FBF9DB#) 23 | (y #01F9BE7DA0E4E84774048058B53202B2704BF688A306092ED533A55E68EABA814C8D62F45AAD8FF30C3055DCA461B7DBA6B78938FC4D69780A830C6457CC107F3D275C21D00E53147C14162176C77169D3BCA586DC30F15F4B482160E276869AA336F38AF7FC3686A764AB5A02C751D921A42B8B9AE8E06918059CD73C424154#) 24 | (x #00943480B228FC0D3D7ADFC91F680FC415E1306333#) 25 | ) 26 | ) 27 | ) 28 | ) 29 | -------------------------------------------------------------------------------- /tests/chatsecure/otr_keystore: -------------------------------------------------------------------------------- 1 | #Tue Nov 15 14:39:30 EST 2011 2 | gptest@jabber.org.privateKey=MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAMtPbcgvf2CAHN4djUb+gCPw/e8Xpeyc9GknS9zs\nJjSCg9vgiKBVlQBceiKAkK8SVVEaA671SS0XO575OK/sAc4j0n2t9QJP1wyGCOhV79WbwhPPEVhs\ncpAHakr9IAW6WdSnwhL/seZLYRKiVGpxXJffwN+sYjH00PulKNxmz2+DAhUAxh9yFSC1uuGk6IR0\ntnVAfsPUt7cCgYBGfHU40n0HgKIkVe3XtX0G3CbiGbR++qaEjNqnfWynggqeeVkYliLaDlVrR4B0\nnLrHZLEcUMO38YKmrwug02acp9P65IcjZ2yaioPBSmV7R6pMGOdJFR3V7Pd5R2+NcUdJd2xSffLf\nrChM82SKqa7b3DOPHkSoIdp/vJiRgikZrwQWAhRE5snYBaoR84hWVdxlumAYkBRUEA\=\= 3 | gptest@jabber.org.publicKey=MIIBtjCCASsGByqGSM44BAEwggEeAoGBAMtPbcgvf2CAHN4djUb+gCPw/e8Xpeyc9GknS9zsJjSC\ng9vgiKBVlQBceiKAkK8SVVEaA671SS0XO575OK/sAc4j0n2t9QJP1wyGCOhV79WbwhPPEVhscpAH\nakr9IAW6WdSnwhL/seZLYRKiVGpxXJffwN+sYjH00PulKNxmz2+DAhUAxh9yFSC1uuGk6IR0tnVA\nfsPUt7cCgYBGfHU40n0HgKIkVe3XtX0G3CbiGbR++qaEjNqnfWynggqeeVkYliLaDlVrR4B0nLrH\nZLEcUMO38YKmrwug02acp9P65IcjZ2yaioPBSmV7R6pMGOdJFR3V7Pd5R2+NcUdJd2xSffLfrChM\n82SKqa7b3DOPHkSoIdp/vJiRgikZrwOBhAACgYAVb/mCnKb7Zl12kPXYTTkCvN4JSvxxhAmb7Nea\nXno2JVd5X/4ubp3M5QGQWvf72FXwUnSILRz6T8gRaEYtuSO3/lY4q5vOAOnVQU6KjH97SKMutwHT\nl9d+zbuoBc4YMASUZa+vKqRZ3a+d15WdlBjtEzB2NbBbnbCJKjfGSmOCbg\=\= 4 | hans@eds.org.FF66E8C909C4DEB51CBD4A02B9E6AF4D6AF215F8.publicKey.verified=true 5 | hans@eds.org.publicKey=MIIBtjCCASsGByqGSM44BAEwggEeAoGBAIaQ8aS9ZyrLhulUFl80zqdsU1XcYjQ5t+kgi52LSd9J\n0k4k7RrVeAnHHxcbyF7usVpkD+KG9V5VrUgYRDLZSAsGPcjITQcEqiv1zkJqog9InoPciOv+qVVA\nwKEwC5tGLEQ8eb9r7O0RQVLg8o7vSTs8yHBg77BDn+0UQsDaTUuTAhUA20hNBdXSrUtNMkkoFgLC\nNjEQq38CgYARy0ipvVMrbQG8+9rsRk7budGuq78EMVHYX18RgDGpwUG28tmwQTc77Q2plwqvw1yp\nRK43gdIq6M2SkD3BpBSHFyK4lp8m+YOSOTyA8DZ6TsdXtolfCKVOvoFBk5jN07QtT4FVkmgomAFy\nIVyX2DoHcnfXGvjEH0ecyh7LkA8nYgOBhAACgYBuKX5DVjpVF/8RqiqDN6RernvCMEjn4OXN6gBN\nmTZqrJg8Mi/gzOofFe40sZ4TzmGZX1lM3nfkjBpsvNkJ/W4QCDVeSTE90RM3B0ohXEwY2KWQ3QrI\nQvU33hRyXJYOVX31nQbQRHy1fgWDuEGCM01RUtrCtIdoxm2Fe2qMt4nINg\=\= 6 | -------------------------------------------------------------------------------- /tests/chatsecure/otr_keystore.ofcaes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/chatsecure/otr_keystore.ofcaes -------------------------------------------------------------------------------- /tests/gajim/guardianproject.info.fpr: -------------------------------------------------------------------------------- 1 | hans@guardianproject.info gptest@guardianproject.info xmpp df953988f5c05788c0327186d06d3bb4ddaaec7f smp 2 | miron@hyper.to gptest@guardianproject.info xmpp 3fed5b9b61a7669b4f140639df65af0233aec76c 3 | steve.wyshywaniuk@gmail.com gptest@guardianproject.info xmpp 367dd4fd52639242eb00a0e37f5d290934b4d37f 4 | baghdadbot@gmail.com gptest@guardianproject.info xmpp 30e756646687d75b61434667883779cd2ad3db1d 5 | -------------------------------------------------------------------------------- /tests/gajim/guardianproject.info.key3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/gajim/guardianproject.info.key3 -------------------------------------------------------------------------------- /tests/gnupg/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/gnupg/pubring.gpg -------------------------------------------------------------------------------- /tests/gnupg/random_seed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/gnupg/random_seed -------------------------------------------------------------------------------- /tests/gnupg/secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/gnupg/secring.gpg -------------------------------------------------------------------------------- /tests/gnupg/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/tests/gnupg/trustdb.gpg -------------------------------------------------------------------------------- /tests/irssi/otr.fp: -------------------------------------------------------------------------------- 1 | bigbear test@example.org IRC fffffffffffffcdfdddddeeeeee8aaaaaad00000 2 | goldy test@example.org IRC 0000111122223444455566666777778889990aab 3 | blue test@example.org IRC bbbbbbbbbb223444455566666777778889990aab smp 4 | testymctest test@example.org IRC eeeeee1122223444455566666777778889990aab manual 5 | -------------------------------------------------------------------------------- /tests/irssi/otr.key: -------------------------------------------------------------------------------- 1 | (privkeys 2 | (account 3 | (name "test@example.org") 4 | (protocol IRC) 5 | (private-key 6 | (dsa 7 | (p #00E35B4CA8D0AED9CF4B1775A0C97789160791D9F2ED8A11EF1F120EDF0E1143232A4C336132C4D759C7A1C093EEF57E879C42BCF2E316A12D47B6189CC9F53F950172BB6D2417EDB2351F87516CFF87F3D01298D62D41607D237E8FD6B32360D2D1DA51C0D16C284F4180C6655887BFFEA7EA5755751C23F26EB8185E1C82A173#) 8 | (q #00A761D47128AD0FC90DB212364A63DD7E685D3C67#) 9 | (g #00CDCB59DF8B48DE44E1E7B3D4A18AFED11396CD7BC9BD5C9314996D48B5110D2F4BF6C3B2F0F2B47DDA2AFE6718177EB4E91E74B7C1E40F86186160337E5BA5CE763C87DAFCCA673540E3A9DF24A5A3B2CD75DB91A18B30AD14864F04B0A924556E6842FFB88932D0AE588705AC4FDABF686940934DD731917D63AF21FF19CA8B#) 10 | (y #00B074FD79609810E2B187A09D403DF8F865D99C717C9E3D0A461D0C37E8957A85B1048830965D72EC376B0C2D2567D60837D5BB6261FB6A4B9BBD6B96DDD5D4E571FBF919AE866DB6438E3D821C501B6E255E89210F0DB90EB9D435ED5B9E62F2644823608249620CEFEC9E5E80B598A1C9A1EDE8E7F1DE5DED9F3A3AD749F7F2#) 11 | (x #0DF19E6EF1D1E7D51818E085F7DDB8CB9F77CA1A#) 12 | ) 13 | ) 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /tests/jitsi/contactlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | wifey 14 | 15 | xcap.resolved=false; 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | gptest@jabber.org 28 | 29 | 30 | 31 | nathanfreitas@gmail.com 32 | 33 | 34 | 35 | fakename(mac.com)@aol.com 36 | 37 | 38 | 39 | hans@eds.org 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | gp.test@jabber.org 59 | 60 | 61 | 62 | gptest@jabber.ccc.de 63 | 64 | 65 | 66 | gptestlaptop@jabber.org 67 | 68 | 69 | 70 | guardiantest@jabber.org 71 | 72 | 73 | 74 | guardianproject@gmail.com 75 | 76 | 77 | 78 | gptest@limun.org 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Wifey 87 | 88 | 89 | 90 | horrendous@gmail.com 91 | 92 | 93 | 94 | gptest@jabber.com 95 | 96 | 97 | 98 | maximum@googlemail.com 99 | 100 | 101 | 102 | hans@guardianproject.info 103 | 104 | 105 | 106 | hans@eds.org 107 | 108 | 109 | 110 | gptest@limum.org 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /tests/keyczar/privatekey-v0.5b.json: -------------------------------------------------------------------------------- 1 | {"publicKey":{"x509":"MIIBuDCCASwGByqGSM44BAEwggEfAoGBAP1_U4EddRIpUt9KnC7s5O 2 | f2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq_xfW6MPbLm1Vs14E7gB00b_JmYLdrmVClpJ-f6A 3 | R7ECLCT7up1_63xhv4O1fnxqimFQ8E-4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAh 4 | UAl2BQjxUjC8yykrmCouuEC_BYHPUCgYEA9-GghdabPd7LvKtcNrhXuXmUr7v6OuqC-VdMCz0Hgm 5 | dRWVeOutRZT-ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN_C_ohNW 6 | Lx-2J6ASQ7zKTxvqhRkImog9_hWuWfBpKLZl6Ae1UlZAFMO_7PSSoDgYUAAoGBAMRSRR0U9NUcWJ 7 | qziTTRgMykYcPOWe7qlAwoPdPdVoYFUKuXCgZNgo15jLrqLdCYi6YDw2ueMBKpUjVW01SM8ispW0 8 | KlSd7HNGMPtTYdq5h7j-HrYgEQwuPbnbZnJLsPwRE2lg-1q-GMwLDUdBlAMajWvNwqT-dbnxbB6M 9 | HSVrAR"}, 10 | "pkcs8":"MIIBSwIBADCCASwGByqGSM44BAEwggEfAoGBAP1_U4EddRIpUt9KnC7s5Of2EbdSPO9 11 | EAMMeP4C2USZpRV1AIlH7WT2NWPq_xfW6MPbLm1Vs14E7gB00b_JmYLdrmVClpJ-f6AR7ECLCT7u 12 | p1_63xhv4O1fnxqimFQ8E-4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAhUAl2BQjxU 13 | jC8yykrmCouuEC_BYHPUCgYEA9-GghdabPd7LvKtcNrhXuXmUr7v6OuqC-VdMCz0HgmdRWVeOutR 14 | ZT-ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN_C_ohNWLx-2J6ASQ 15 | 7zKTxvqhRkImog9_hWuWfBpKLZl6Ae1UlZAFMO_7PSSoEFgIUIDVT7v0qg5FgzH3acadcaJ9Av9M"} 16 | -------------------------------------------------------------------------------- /tests/keyczar/privatekey-v0.6b.json: -------------------------------------------------------------------------------- 1 | { 2 | "publicKey": { 3 | "q": "AJJfsQZrhUV8p6TmpPqa084JwX9j", 4 | "p": "AIAAAAAAAAXxHhQxJRZ-PPj2BDrHHLV8c8pX6nyOLAW3Bc7CX_SfBiGH2VyImoz6JlAOZi 5 | 6x_XspxdUvpTjV7J9uO9hwnF31m3SQjdkZW2DQDb5OS1rW_4MGrTJCktKtlZz7f8_5AoO8 6 | yHSY2XWNDqrpBEiNvaTX1ttQ59nREiR1", 7 | "y": "AGlQuRpbat4drE_fcdSZrEVfS6Fme3tNfUoJVRec1pUhoSo9PBHKFx3lbBmI8Vnub8vuY1 8 | nM2yTadOZ8H4-TYxB5JNMVTK7vLNdVcWvUUF9zRZCwps1bl0_Al29X0I1iQYJN6Klxi_Qb 9 | KaSf5PhfXLVom9bJYp7_TwZCouaab296", 10 | "g": "AES5hk-DKXP__t6yDsXIdykf7lhSKHqQCW5H2V5dMg8JkoFBSP7 mIvaCHT4IxoxdM2AIp 11 | Wgcoi5XSrd_hD2sjNa1JHTb9BUh31dHJLym6rTsV12ClN6f78Cjt0oKFIRI__yWn9KM-vL 12 | Esjpd10VHlPfbEgKYePCnXFt7Y78G0wGr", 13 | "size": 1024 14 | }, 15 | "x": "AGLJry5Q0CZo9cH6XRYd2ZZZppwg", 16 | "size": 1024, 17 | } 18 | -------------------------------------------------------------------------------- /tests/keyczar/publickey-v0.5b.json: -------------------------------------------------------------------------------- 1 | {"x509":"MIIBuDCCASwGByqGSM44BAEwggEfAoGBAP1_U4EddRIpUt9KnC7s5O 2 | f2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq_xfW6MPbLm1Vs14E7gB00b_JmYLdrmVClpJ-f6A 3 | R7ECLCT7up1_63xhv4O1fnxqimFQ8E-4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAh 4 | UAl2BQjxUjC8yykrmCouuEC_BYHPUCgYEA9-GghdabPd7LvKtcNrhXuXmUr7v6OuqC-VdMCz0Hgm 5 | dRWVeOutRZT-ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN_C_ohNW 6 | Lx-2J6ASQ7zKTxvqhRkImog9_hWuWfBpKLZl6Ae1UlZAFMO_7PSSoDgYUAAoGBAMRSRR0U9NUcWJ 7 | qziTTRgMykYcPOWe7qlAwoPdPdVoYFUKuXCgZNgo15jLrqLdCYi6YDw2ueMBKpUjVW01SM8ispW0 8 | KlSd7HNGMPtTYdq5h7j-HrYgEQwuPbnbZnJLsPwRE2lg-1q-GMwLDUdBlAMajWvNwqT-dbnxbB6M 9 | HSVrAR"} 10 | -------------------------------------------------------------------------------- /tests/keyczar/publickey-v0.6b.json: -------------------------------------------------------------------------------- 1 | { 2 | "q": "AJJfsQZrhUV8p6TmpPqa084JwX9j", 3 | "p": "AIAAAAAAAAXxHhQxJRZ-PPj2BDrHHLV8c8pX6nyOLAW3Bc7CX_SfBiGH2VyImoz6JlAOZi 4 | 6x_XspxdUvpTjV7J9uO9hwnF31m3SQjdkZW2DQDb5OS1rW_4MGrTJCktKtlZz7f8_5AoO8 5 | yHSY2XWNDqrpBEiNvaTX1ttQ59nREiR1", 6 | "y": "AGlQuRpbat4drE_fcdSZrEVfS6Fme3tNfUoJVRec1pUhoSo9PBHKFx3lbBmI8Vnub8vuY1 7 | nM2yTadOZ8H4-TYxB5JNMVTK7vLNdVcWvUUF9zRZCwps1bl0_Al29X0I1iQYJN6Klxi_Qb 8 | KaSf5PhfXLVom9bJYp7_TwZCouaab296", 9 | "g": "AES5hk-DKXP__t6yDsXIdykf7lhSKHqQCW5H2V5dMg8JkoFBSP7 mIvaCHT4IxoxdM2AIp 10 | Wgcoi5XSrd_hD2sjNa1JHTb9BUh31dHJLym6rTsV12ClN6f78Cjt0oKFIRI__yWn9KM-vL 11 | Esjpd10VHlPfbEgKYePCnXFt7Y78G0wGr", 12 | "size": 1024 13 | } 14 | -------------------------------------------------------------------------------- /tests/kopete/fingerprints: -------------------------------------------------------------------------------- 1 | gptest@jabber.ccc.de gptest@jabber.org/ prpl-jabber a2cd015572d256703e7624840f91209160185410 2 | gptest@jabber.ccc.de gptest@limun.org/ prpl-jabber a2cd015572d256703e7624840f91209160185410 verified 3 | hans@eds.org gptest@jabber.org/ prpl-jabber ff66e8c909c4deb51cbd4a02b9e6af4d6af215f8 4 | hans@eds.org gptest@limun.org/ prpl-jabber f83fe8c909c4deb51c234574bab3e4f66af215f8 5 | n8fr8@foo_com gptest@mycomputer prpl-bonjour 92967e4743bc730e0a3396be1a6ea365cc38f0f3 6 | gptest@jabber_ccc_de gptest@mycomputer prpl-bonjour a2cd015572d256703e7624840f91209160185410 verified 7 | -------------------------------------------------------------------------------- /tests/kopete/privkeys: -------------------------------------------------------------------------------- 1 | (privkeys 2 | (account 3 | (name "gptest@limun.org/") 4 | (protocol prpl-jabber) 5 | (private-key 6 | (dsa 7 | (p #00B20F6CADBA15D628E861586099F9C18E68C822D3C02E21B57B7464D5A664BF40E6EB45F1931E9CD3E098890BF2E09662DFBE2B530CBDE8FF1D542333C02DFEAA904A970C29A7403F9328B2BC6D8EA6233E484B4E9B28D63B83CB1FC13DFDF493D80DEE4AB52DDDC3C19AC9144C16E46B491327C09E688AE582B9F8D56E271A2B#) 8 | (q #00EAB4FEDFB2C033131204EADDFD9EFA21B50D0AE9#) 9 | (g #00AEC459E479F4A750672567E3B69F3461A54480222580EABF14DA0D520EB0F6D7A74B2287A5A4628C8F966FB5D2AD77C09F4053B9D08FA001BA167CA18CC05177B864FE2EED8C759C4475E2A6B4284B6BB0A2CB856EBD45BB299E30709F09D260672CC850E0A12AD8C507B34080377E9C0E401A3F84B0783EA87112B1DBDA9B0A#) 10 | (y #29A9DE8FD266F19188246B34B613F4717DF106CD8B2179C69CF4172196B0FD562DBB3069428B9315C819758A3CBF0977A7B2054600C43554DF12AFE496FEF79DF5A14300E3F5F2A0E26E8EBB576A52BD423929718D4EA689BD46A021BCD96A5BAE8F83E9C0FF46BE63BE0239A5FC1EE901AA56E2E98E4CDA7D069570E5BC3615#) 11 | (x #00B8787BA6DEEEC7496EACCA84FF723F6E9EA5F6A5#) 12 | ) 13 | ) 14 | ) 15 | (account 16 | (name "gptest@jabber.org/") 17 | (protocol prpl-jabber) 18 | (private-key 19 | (dsa 20 | (p #00CD96479A2F404FB600F9C85EE3DCD69FDF93F217AEE54A1286069983BA7731D0C73C7CB3B8CFA482A0AF6FF906E470EB4EF7F4D201D087AEDBF0086710F3039CBF42358C1BFA7D86A36332E21D32BE31538571CBC8D4F281DDD1076BA2B29B549ED29B3C19C341AEF83C80157E87FF2930B5E15C84A09AFCE28A06E06A62BCEF#) 21 | (q #00D5B4647E688974D1B6B199E1A59AB2F985DBCE01#) 22 | (g #0093E333135FCBCE68FC6E410B304482F2F95D82BF53534C3636EADB0C22241CF35BD294B096070DC08138EFE73B03C88FD444595974E9455274F695147AF9D46B85286B4CFEF3D00BCA1D3BEB8C7EFFBA08132E1A1E4D9F115B863C52C72971F4695758354FC3BE3C4A45AF6A47747B59733905C33EE86ED68D9D90494877AE33#) 23 | (y #362C06B9CDD67FC4E7A7A62289D6C1E8BA061A024946A5ACC1A7DC70F6B03D99A1D3B3215D20BC4F8458EEC3F31E1391E9B519FDCDCC3CF0FBA38F8A7213551B32D59DE655F506633FB6B0EA94C4174D227DB614EF6723AB057B40CF36E2A414D0A8DCF223EE7EDD851793C4DC92282C79503030045D49A0ABCC3C6CC4080909#) 24 | (x #24FE542B1C7DC8337F6F8030C7D639B7BF091B40#) 25 | ) 26 | ) 27 | ) 28 | (account 29 | (name "gptest@mycomputer") 30 | (protocol prpl-bonjour) 31 | (private-key 32 | (dsa 33 | (p #00B727869A051E6F00EA571BEC0A3EE8D9DBE91CBEA9AF8CE1122B18F9EBEBE6B2F4CA2D0A481D0EC9363C56F389EA7AC1F631798989797FE4868AAA18E005D3A7FAA631047F73EC9291246369495CB9ADD4E6447BF959B2FA6AA03DF5B781A40A40B7693AF5B1B466DB1BE14CFB92C5680D6CA9ADD1A4F134C02A0C4CA32AFEC7#) 34 | (q #008952F4EA8C624566E2B720734562EF2EA9ADE76D#) 35 | (g #2570F44667EC9FF45456EEEBC0A000FA9D24CD5DF1EBB59C897E07D4F98CB6798011235BC5075BE96AADE338D02CA31CCF6393ED2399CB4263429170B0C841A17BE2E2B43914C004AAAE05CA544723215D99D809E0C28C13D5DFFF8D381323D54C4B785E8C922DC7A4A78F9324161BAC2A9745E667594C89BEDE7997B9E2545F#) 36 | (y #009EEBE6739180FC47E744F1ECB4321B11255787890123578902350C052CE18985F1F6F37986A5D8EDBDB524264879128793489172475E60A20EACFDEB168D05921E242B5A3A78912389ABBADF742B4BAABE35345E3D9E5F13BBF5BB8806CEFABE9B227219724889AB897234D4295CFE7EF1DDCE997BA504304BB87F4EA18A4C1A#) 37 | (x #072349EAF19604CCE2FEDDBEA2342CCF93C71224#) 38 | ) 39 | ) 40 | ) 41 | ) 42 | -------------------------------------------------------------------------------- /tests/openssl/parse-private-key-PKCS#8.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import pyjavaproperties 4 | import imp 5 | 6 | errors = imp.load_source('errors', '../../otrapps/errors.py') 7 | util = imp.load_source('util', '../../otrapps/util.py') 8 | 9 | filename = 'dsa-key.properties' 10 | p = pyjavaproperties.Properties() 11 | p.load(open(filename)) 12 | for item in p.items(): 13 | if item[0] == 'privateKey': 14 | privdict = util.ParsePkcs8(item[1]) 15 | print 'privdict: ', 16 | print privdict 17 | elif item[0] == 'publicKey': 18 | pubdict = util.ParseX509(item[1]) 19 | print 'pubdict: ', 20 | print pubdict 21 | -------------------------------------------------------------------------------- /tests/openssl/useful-commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # this script uses openssl to generate a standard OTR DSA key or 3072 bits 4 | 5 | openssl_key=private-key-openssl.pem 6 | x509_key=public-key-PKCS\#1-X.509.pem 7 | pkcs8_key=private-key-PKCS\#8.pem 8 | propfile=dsa-key.properties 9 | 10 | # generate a new unencrypted DSA key 11 | openssl dsaparam -out dsaparam.pem 3072 12 | openssl gendsa -out $openssl_key dsaparam.pem 13 | 14 | # convert public key to PKCS#1/X.509 format 15 | openssl dsa -in $openssl_key -pubout -out $x509_key 16 | 17 | # convert private key to a PKCS#8 format 18 | openssl pkcs8 -nocrypt -in $openssl_key -topk8 -out $pkcs8_key 19 | 20 | # convert PKCS#8 private key to a java properties file 21 | printf "privateKey=" > $propfile 22 | echo `cat $pkcs8_key | grep -v -- '-----'` | sed 's, ,,g' | sed 's,=,\\=,g' >> $propfile 23 | 24 | # convert PKCS #1/X.509 public key to a java properties file 25 | printf "publicKey=" >> $propfile 26 | echo `cat $x509_key | grep -v -- '-----'` | sed 's, ,,g' | sed 's,=,\\=,g' >> $propfile 27 | -------------------------------------------------------------------------------- /tests/pidgin/otr.fingerprints: -------------------------------------------------------------------------------- 1 | gptest@jabber.ccc.de gptest@jabber.org/ prpl-jabber a2cd015572d256703e7624840f91209160185410 2 | gptest@jabber.ccc.de gptest@limun.org/ prpl-jabber a2cd015572d256703e7624840f91209160185410 verified 3 | hans@eds.org gptest@jabber.org/ prpl-jabber ff66e8c909c4deb51cbd4a02b9e6af4d6af215f8 4 | hans@eds.org gptest@limun.org/ prpl-jabber f83fe8c909c4deb51c234574bab3e4f66af215f8 5 | n8fr8@foo_com gptest@mycomputer prpl-bonjour 92967e4743bc730e0a3396be1a6ea365cc38f0f3 6 | gptest@jabber_ccc_de gptest@mycomputer prpl-bonjour a2cd015572d256703e7624840f91209160185410 verified 7 | -------------------------------------------------------------------------------- /tests/pidgin/otr.private_key: -------------------------------------------------------------------------------- 1 | (privkeys 2 | (account 3 | (name "gptest@limun.org/") 4 | (protocol prpl-jabber) 5 | (private-key 6 | (dsa 7 | (p #00B20F6CADBA15D628E861586099F9C18E68C822D3C02E21B57B7464D5A664BF40E6EB45F1931E9CD3E098890BF2E09662DFBE2B530CBDE8FF1D542333C02DFEAA904A970C29A7403F9328B2BC6D8EA6233E484B4E9B28D63B83CB1FC13DFDF493D80DEE4AB52DDDC3C19AC9144C16E46B491327C09E688AE582B9F8D56E271A2B#) 8 | (q #00EAB4FEDFB2C033131204EADDFD9EFA21B50D0AE9#) 9 | (g #00AEC459E479F4A750672567E3B69F3461A54480222580EABF14DA0D520EB0F6D7A74B2287A5A4628C8F966FB5D2AD77C09F4053B9D08FA001BA167CA18CC05177B864FE2EED8C759C4475E2A6B4284B6BB0A2CB856EBD45BB299E30709F09D260672CC850E0A12AD8C507B34080377E9C0E401A3F84B0783EA87112B1DBDA9B0A#) 10 | (y #29A9DE8FD266F19188246B34B613F4717DF106CD8B2179C69CF4172196B0FD562DBB3069428B9315C819758A3CBF0977A7B2054600C43554DF12AFE496FEF79DF5A14300E3F5F2A0E26E8EBB576A52BD423929718D4EA689BD46A021BCD96A5BAE8F83E9C0FF46BE63BE0239A5FC1EE901AA56E2E98E4CDA7D069570E5BC3615#) 11 | (x #00B8787BA6DEEEC7496EACCA84FF723F6E9EA5F6A5#) 12 | ) 13 | ) 14 | ) 15 | (account 16 | (name "gptest@jabber.org/") 17 | (protocol prpl-jabber) 18 | (private-key 19 | (dsa 20 | (p #00CD96479A2F404FB600F9C85EE3DCD69FDF93F217AEE54A1286069983BA7731D0C73C7CB3B8CFA482A0AF6FF906E470EB4EF7F4D201D087AEDBF0086710F3039CBF42358C1BFA7D86A36332E21D32BE31538571CBC8D4F281DDD1076BA2B29B549ED29B3C19C341AEF83C80157E87FF2930B5E15C84A09AFCE28A06E06A62BCEF#) 21 | (q #00D5B4647E688974D1B6B199E1A59AB2F985DBCE01#) 22 | (g #0093E333135FCBCE68FC6E410B304482F2F95D82BF53534C3636EADB0C22241CF35BD294B096070DC08138EFE73B03C88FD444595974E9455274F695147AF9D46B85286B4CFEF3D00BCA1D3BEB8C7EFFBA08132E1A1E4D9F115B863C52C72971F4695758354FC3BE3C4A45AF6A47747B59733905C33EE86ED68D9D90494877AE33#) 23 | (y #362C06B9CDD67FC4E7A7A62289D6C1E8BA061A024946A5ACC1A7DC70F6B03D99A1D3B3215D20BC4F8458EEC3F31E1391E9B519FDCDCC3CF0FBA38F8A7213551B32D59DE655F506633FB6B0EA94C4174D227DB614EF6723AB057B40CF36E2A414D0A8DCF223EE7EDD851793C4DC92282C79503030045D49A0ABCC3C6CC4080909#) 24 | (x #24FE542B1C7DC8337F6F8030C7D639B7BF091B40#) 25 | ) 26 | ) 27 | ) 28 | (account 29 | (name "gptest@mycomputer") 30 | (protocol prpl-bonjour) 31 | (private-key 32 | (dsa 33 | (p #00B727869A051E6F00EA571BEC0A3EE8D9DBE91CBEA9AF8CE1122B18F9EBEBE6B2F4CA2D0A481D0EC9363C56F389EA7AC1F631798989797FE4868AAA18E005D3A7FAA631047F73EC9291246369495CB9ADD4E6447BF959B2FA6AA03DF5B781A40A40B7693AF5B1B466DB1BE14CFB92C5680D6CA9ADD1A4F134C02A0C4CA32AFEC7#) 34 | (q #008952F4EA8C624566E2B720734562EF2EA9ADE76D#) 35 | (g #2570F44667EC9FF45456EEEBC0A000FA9D24CD5DF1EBB59C897E07D4F98CB6798011235BC5075BE96AADE338D02CA31CCF6393ED2399CB4263429170B0C841A17BE2E2B43914C004AAAE05CA544723215D99D809E0C28C13D5DFFF8D381323D54C4B785E8C922DC7A4A78F9324161BAC2A9745E667594C89BEDE7997B9E2545F#) 36 | (y #009EEBE6739180FC47E744F1ECB4321B11255787890123578902350C052CE18985F1F6F37986A5D8EDBDB524264879128793489172475E60A20EACFDEB168D05921E242B5A3A78912389ABBADF742B4BAABE35345E3D9E5F13BBF5BB8806CEFABE9B227219724889AB897234D4295CFE7EF1DDCE997BA504304BB87F4EA18A4C1A#) 37 | (x #072349EAF19604CCE2FEDDBEA2342CCF93C71224#) 38 | ) 39 | ) 40 | ) 41 | ) 42 | -------------------------------------------------------------------------------- /tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | copy_accounts_files () { 6 | app=$1 7 | tests="$2" 8 | outdir="$3" 9 | test -d $outdir || mkdir -p $outdir 10 | if [ $app = 'adium' ]; then 11 | cp $tests/adium/Accounts.plist $outdir/ 12 | elif [ $app = 'jitsi' ]; then 13 | cp $tests/jitsi/contactlist.xml \ 14 | $tests/jitsi/sip-communicator.properties \ 15 | $outdir/ 16 | elif [ $app = 'pidgin' ]; then 17 | cp $tests/pidgin/accounts.xml $outdir/ 18 | elif [ $app = 'gajim' ]; then 19 | cp $tests/gajim/config $outdir/ 20 | fi 21 | } 22 | 23 | 24 | tmpdir=`mktemp -d /tmp/keysync-XXXXXXXXXXXXXX` 25 | testbase=`pwd` 26 | projectbase=$(dirname $testbase) 27 | 28 | # allow the location of the script to be overridden 29 | if [ -z $keysync ]; then 30 | keysync="./keysync" 31 | fi 32 | 33 | 34 | echo '========================================================================' 35 | echo "Run each SAMENESS test" 36 | echo '========================================================================' 37 | cd $projectbase 38 | for inapp in adium pidgin; do 39 | for outapp in adium pidgin; do 40 | echo '' 41 | echo '' 42 | echo '------------------------------------------------------------------------' 43 | echo "Run $inapp-$outapp SAMENESS tests" 44 | echo '------------------------------------------------------------------------' 45 | tests=$testbase/SAMENESS 46 | outdir=$tmpdir/SAMENESS-$inapp-$outapp 47 | copy_accounts_files $outapp $tests $outdir 48 | $keysync --test $tests -i $inapp -o $outapp --output-folder $outdir 49 | for f in $tests/$outapp/*; do 50 | echo '--------------------------------' 51 | echo $f 52 | # remove '|| true' once these tests actually pass: 53 | diff -u $f $outdir/$(basename $f) || true 54 | # nice way to see the diff: 55 | # meld $f $outdir/$(basename $f) 56 | done 57 | done 58 | done 59 | 60 | 61 | echo '========================================================================' 62 | echo "Run each python file's __main__ tests" 63 | echo '========================================================================' 64 | cd $projectbase/otrapps 65 | for app in adium chatsecure gajim gnupg irssi jitsi kopete pidgin xchat util; do 66 | echo '' 67 | echo '' 68 | echo '------------------------------------------------------------------------' 69 | echo "Run $app's __main__ tests" 70 | echo '------------------------------------------------------------------------' 71 | python ./$app.py 72 | done 73 | 74 | echo '========================================================================' 75 | echo "Merge all test files into an app's format" 76 | echo '========================================================================' 77 | cd $projectbase 78 | for app in adium chatsecure gajim irssi jitsi kopete pidgin xchat; do 79 | echo '------------------------------------------------------------------------' 80 | echo "Merge all test files into $app's format" 81 | echo '------------------------------------------------------------------------' 82 | tests=$testbase 83 | outdir=$tmpdir/merge-into-$app 84 | mkdir $outdir 85 | copy_accounts_files $app $tests $outdir 86 | $keysync --test $tests \ 87 | -i adium -i gnupg -i irssi -i jitsi -i pidgin -i xchat \ 88 | -o $app \ 89 | --output-folder $outdir 90 | done 91 | 92 | 93 | echo '========================================================================' 94 | echo "Convert each app to each other app" 95 | echo '========================================================================' 96 | cd $projectbase 97 | for inapp in adium chatsecure gajim gnupg irssi jitsi kopete pidgin xchat; do 98 | for outapp in adium chatsecure gajim gnupg irssi jitsi kopete pidgin xchat; do 99 | echo '------------------------------------------------------------------------' 100 | echo "Convert $inapp to $outapp " 101 | echo '------------------------------------------------------------------------' 102 | tests=$testbase 103 | outdir=$tmpdir/each-$inapp-to-$outapp 104 | mkdir $outdir 105 | copy_accounts_files $outapp $tests $outdir 106 | $keysync --test $tests --input $inapp --output $outapp --output-folder $outdir 107 | done 108 | done 109 | 110 | 111 | echo '========================================================================' 112 | echo "decrypt ChatSecure file to all apps" 113 | echo '========================================================================' 114 | cd $projectbase 115 | inapp=chatsecure 116 | for outapp in adium gajim gnupg irssi jitsi kopete pidgin xchat; do 117 | echo '------------------------------------------------------------------------' 118 | echo "Convert $inapp to $outapp " 119 | echo '------------------------------------------------------------------------' 120 | tests=$testbase 121 | outdir=$tmpdir/decrypt-$inapp-to-$outapp 122 | mkdir $outdir 123 | cp $testbase/chatsecure/otr_keystore.ofcaes $outdir/ 124 | copy_accounts_files $outapp $tests $outdir 125 | echo "6QpT40Omhp6YRX73BzxnPlSvvr7ZsPP6VaS4aqWOyqE=" | \ 126 | $keysync --test $tests --input $inapp --output $outapp --output-folder $outdir 127 | done 128 | 129 | 130 | echo $tmpdir 131 | ls -l $tmpdir 132 | -------------------------------------------------------------------------------- /tests/xchat/otr.fp: -------------------------------------------------------------------------------- 1 | bigbear test@example.org IRC fffffffffffffcdfdddddeeeeee8aaaaaad00000 2 | goldy test@example.org IRC 0000111122223444455566666777778889990aab 3 | blue test@example.org IRC bbbbbbbbbb223444455566666777778889990aab smp 4 | testymctest test@example.org IRC eeeeee1122223444455566666777778889990aab manual 5 | -------------------------------------------------------------------------------- /tests/xchat/otr.key: -------------------------------------------------------------------------------- 1 | (privkeys 2 | (account 3 | (name "test@example.org") 4 | (protocol IRC) 5 | (private-key 6 | (dsa 7 | (p #00E35B4CA8D0AED9CF4B1775A0C97789160791D9F2ED8A11EF1F120EDF0E1143232A4C336132C4D759C7A1C093EEF57E879C42BCF2E316A12D47B6189CC9F53F950172BB6D2417EDB2351F87516CFF87F3D01298D62D41607D237E8FD6B32360D2D1DA51C0D16C284F4180C6655887BFFEA7EA5755751C23F26EB8185E1C82A173#) 8 | (q #00A761D47128AD0FC90DB212364A63DD7E685D3C67#) 9 | (g #00CDCB59DF8B48DE44E1E7B3D4A18AFED11396CD7BC9BD5C9314996D48B5110D2F4BF6C3B2F0F2B47DDA2AFE6718177EB4E91E74B7C1E40F86186160337E5BA5CE763C87DAFCCA673540E3A9DF24A5A3B2CD75DB91A18B30AD14864F04B0A924556E6842FFB88932D0AE588705AC4FDABF686940934DD731917D63AF21FF19CA8B#) 10 | (y #00B074FD79609810E2B187A09D403DF8F865D99C717C9E3D0A461D0C37E8957A85B1048830965D72EC376B0C2D2567D60837D5BB6261FB6A4B9BBD6B96DDD5D4E571FBF919AE866DB6438E3D821C501B6E255E89210F0DB90EB9D435ED5B9E62F2644823608249620CEFEC9E5E80B598A1C9A1EDE8E7F1DE5DED9F3A3AD749F7F2#) 11 | (x #0DF19E6EF1D1E7D51818E085F7DDB8CB9F77CA1A#) 12 | ) 13 | ) 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /win32/README.md: -------------------------------------------------------------------------------- 1 | # Building On Windows 2 | 3 | Either a Windows machine or a Linux box with wine installed will do to produce a w32 binary. 4 | 5 | ![](w32-screenshot.png) 6 | 7 | ## Download Prerequisites 8 | 9 | **Automatic Download** (Recommended) 10 | 11 | 1. Execute the`fetch-and-verify.sh` script 12 | 3. Copy all the files to your windows computer 13 | 4. Download the [VS 2008 redistributable][vsredist] 14 | 15 | **Manual Download** 16 | 17 | * [Git][git] 18 | * [Python 2.7][py27] ([sig][pysig]) 19 | * [Python Windows 32 Bindings][pywin32] 20 | * [PyInstaller Development Version][pyinst] 21 | * [Setuptools Bootstrap][setuptools] 22 | * [MingW Installer][mingw] 23 | * [OpenSSL-Win32][openssl] 24 | * [MS VS 2008 redistributable][vsredist] 25 | * [PyCrypto Source][pycrypto] ([sig][pycryptosig]) 26 | * [Pure Python OTR Source][potr] 27 | * [Pidgin][pidgin] ([sig][pidginsig]) 28 | * [Pidgin OTR Plugin][pidgin-otr] ([sig][pidgin-otrsig]) 29 | 30 | **Builds on Linux** 31 | 32 | Install wine: 33 | 34 | * Debian/Ubuntu: `sudo apt-get install wine` 35 | * Fedora: `sudo yum install wine` 36 | 37 | 38 | ## Install Prerequisites 39 | 40 | ### MingW 41 | 42 | 1. Execute the installer 43 | 2. Follow the wizard 44 | 3. When prompted in the installer choose this options: 45 | 46 | ``` 47 | C compiler 48 | C++ compiler 49 | Msys basic system 50 | MinGW Developer Toolkit 51 | ``` 52 | 53 | ### Git 54 | 55 | 1. Execute the installer 56 | 2. Choose to add git to the PATH 57 | 58 | ### OpenSSL 59 | 60 | 1. Execute the vcredist_x86.exe package 61 | 2. Install it completely 62 | 3. Execute the OpenSSL installer 63 | 4. When prompted choose "Copy OpenSSL DLLs to: The OpenSSL binaries (/bin) directory" 64 | 65 | ### Python 66 | 67 | 1. Execute the Python installer, use default options 68 | * Linux+Wine: you must use `msiexec /i python-2.7.5.msi` 69 | 70 | ## Post Python Install Configuration 71 | 72 | **Configure the PATH environment variable** 73 | 74 | Windows: 75 | 76 | 1. Right-click on My Computer and select properties 77 | 2. Go to the 'Advanced' section and select 'Environment variables' 78 | 3. Select 'Path' from the 'System Variables' and click the 'Edit button' 79 | 4. Append the following to your PATH variable: 80 | 81 | ``` 82 | C:\MinGW\bin;C:\MinGW\msys\1.0\bin;C:\Python27;C:\Python27\Scripts;C:\OpenSSL-Win32\bin; 83 | ``` 84 | 85 | Linux+Wine: 86 | 87 | 1. Open regedit using the `regedit` command 88 | 2. Edit `HKEY_CURRENT_USER/Environment` 89 | 3. Add a new String value called `PATH` using the value above 90 | 91 | **Set Python to use MinGW** 92 | 93 | 1. Create the following file: C:\Python27\Lib\distutils\distutils.cfg 94 | 2. Append: 95 | 96 | ``` 97 | [build] 98 | compiler=mingw32 99 | ``` 100 | 3. Save and close 101 | 102 | **Fix -mno-cygwin error** 103 | 104 | To prevent the following error when building Python modules, we must edit a distutils file. 105 | 106 | gcc: error: unrecognized command line option '-mno-cygwin' 107 | 108 | 1. Edit the file `C:\Python27\Lib\distutils\cygwinccompiler.py` 109 | 2. Look for the class: `Mingw32CCompiler` around line 297 110 | 3. Find the block that beings with 'self.set_executables(' 111 | 4. Delete all the '-mno-cygwin' flags 112 | 5. Save and close 113 | 114 | 115 | ## Install Python Dependencies 116 | 117 | Note: your $HOME when using MinGW shell is in C:\MinGW\msys\1.0\home\USERNAME 118 | 119 | ### setuptools 120 | 121 | 1. Open your MinGW Shell 122 | * Windows: Start -> MinGW -> MinGW Shell 123 | * Linux+Wine: `wine cmd.exe /C C:\MinGW/msys/1.0/msys.bat` 124 | 2. cd to where you downloaded [ez_setup.py][setuptools] 125 | * Windows: Access the normal windows FS using the /c/ path (i.e., /c/ corresponds to C:\) 126 | * Linux+Wine: Access your native root fs with /z/ 127 | 3. Execute: 128 | 129 | ```bash 130 | python ez_setup.py 131 | ``` 132 | 133 | ### Python W32 Bindings 134 | 135 | 1. Execute the Python installer, use default options 136 | 137 | *Note*: If using a virtual environment (virtualenv), then you must open a 138 | command shell that has your virtualenv activated and execute: 139 | 140 | easy_install /c/path/to/pywin32-218.win32-py2.7.exe 141 | 142 | Otherwise pywin32 will be installed into the system site-packages dir, and not 143 | be available to your virtualenv. 144 | 145 | 146 | ### PyCrypto 147 | 148 | For some reason we must install PyCrypto this way, instead of relying on it to 149 | be installed via setup.py along with the rest of the dependencies. 150 | 151 | 1. cd to your extracted pycrypto dir: `cd pycrypto-2.6` 152 | 2. `python seutp.py install` 153 | 154 | 155 | ### KeySync 156 | 157 | 1. `cd keysync` 158 | 2. `python setup.py install` 159 | 160 | This will download, compile, and install all the remaining python modules. 161 | 162 | ## Verify Everything Works 163 | 164 | At this point, all the dependencies for keysync should be installed 165 | and functioning, so keysync should work. You can test it by running keysync-gui: 166 | 167 | You'll probably want to install Pidgin, pidgin-otr, and configure an account 168 | before running keysync. 169 | 170 | ```bash 171 | cd keysync 172 | python keysync-gui 173 | ``` 174 | Do not proceed unless the GUI pops up and the app functions as expected. 175 | 176 | ## Create Windows Executable with PyInstaller 177 | 178 | Note: Due to [bug 651](http://www.pyinstaller.org/ticket/651) the development 179 | version of PyInstaller must be used. 180 | 181 | KeySync's setup.py script automatically installs pyinstaller, so all you need 182 | to do is execute pyinstaller and point it at the script you want to turn into 183 | an EXE. Build the package: 184 | 185 | ```bash 186 | pyinstaller --onefile keysync-gui.spec 187 | ``` 188 | 189 | You can omit the `--onefile` parameter, see the [PyInstaller 190 | Manual](http://htmlpreview.github.io/?https://github.com/pyinstaller/pyinstaller/blob/develop/doc/Manual.html) 191 | for what exactly the consequences of both modes are. 192 | 193 | If the process succeeds, then check the `dist/` directory 194 | inside the pyinstaller directory. 195 | 196 | # Security Considerations 197 | 198 | Unfortunately most of the dependencies and toolchain used above are not 199 | available to download over HTTPS nor do they have signatures to verify. Even 200 | MinGW lacks any sort of secure download, so securely building the dependencies from 201 | source may not be possible. 202 | 203 | Also, according to [this ticket](http://sourceforge.net/p/pywin32/bugs/519/) 204 | the Python Win32 bindings are not easily buildable with MinGW. 205 | 206 | [git]: http://git-scm.com/download/win 207 | [py27]: http://www.python.org/ftp/python/2.7.5/python-2.7.5.msi 208 | [pysig]: http://www.python.org/ftp/python/2.7.5/python-2.7.5.msi.asc 209 | [pywin32]: http://downloads.sourceforge.net/project/pywin32/pywin32/Build%20218/pywin32-218.win32-py2.7.exe?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fpyw# 210 | [pyinst]: https://github.com/pyinstaller/pyinstaller/tarball/develop 211 | [setuptools]: https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py 212 | [mingw]: http://downloads.sourceforge.net/project/mingw/Installer/mingw-get-inst/mingw-get-inst-20120426/mingw-get-inst-20120426.exe?r=&use_mirror=superb# 213 | [openssl]: https://slproweb.com/download/Win32OpenSSL-1_0_1e.exe 214 | [vsredist]: http://www.microsoft.com/en-us/download/details.aspx?id=29 215 | [pycrypto]: https://pypi.python.org/packages/source/p/pycrypto/pycrypto-2.6.tar.gz#md5=88dad0a270d1fe83a39e0467a66a22bb 216 | [pycryptosig]: https://pypi.python.org/packages/source/p/pycrypto/pycrypto-2.6.tar.gz.asc 217 | [potr]: https://github.com/afflux/pure-python-otr/archive/1.0.0beta6.zip 218 | [pidgin]: http://downloads.sourceforge.net/project/pidgin/Pidgin/2.10.7/pidgin-2.10.7-offline.exe?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fpidgin%2Ffil# 219 | [pidginsig]: http://downloads.sourceforge.net/project/pidgin/Pidgin/2.10.7/pidgin-2.10.7-offline.exe.asc?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fpidgin%2# 220 | [pidgin-otr]: http://www.cypherpunks.ca/otr/binaries/windows/pidgin-otr-4.0.0-1.exe 221 | [pidgin-otrsig]: http://www.cypherpunks.ca/otr/binaries/windows/pidgin-otr-4.0.0-1.exe.asc 222 | 223 | 224 | -------------------------------------------------------------------------------- /win32/cypherpunks-otr-pubkey.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: SKS 1.1.0 3 | 4 | mQGiBEGuU5kRBACNWahvxEOQ1QN0+ds1ji5JR0VtAyPhOQn3m1FSexgyzvNzVClYpx/7nvli 5 | mKabImUHQRaOEln+7/lFz3aoMyHQUJaa8ftc6GwgpBCOQkk8itPUv2TgjkQb/DrCtIhGgRay 6 | tph35i8gCHlU7Dy7HPIfCrfhtDgxHAOLAJEx+qWvlwCg1sgrsDjCQ9w24m1z/zMgEeHcwsME 7 | AIqt6SqLlQqsNrj+cLyFjOLUN1u/v4HMxq2HK04qiusNye+/RwNI0suZX2hPy9NE6COfOJnP 8 | +j+Tyl22Xgeq1YFt+NJXUeV4iJ/vpT86stoC0GDNyV7MMSee0QS+S70vpOK73EQd2CH9LDks 9 | VEuEhWeUUWETs7brRFpU55WO/Fy7A/4uv/jypgnAWGdq6908MTU3PCjQ/nOYH55xKELaasAZ 10 | 3Zsqe+EYn87JTyaL2NQvguWX0zVZCzDlM1MtQizEOZbWeiOhyYCzqIVNf9Ao5SSWu2czrLx6 11 | E93kI57EezGhgOaZozZ9/l37F/pENHcu4t15JIcBD4YSdjUZqGgPSM4sY7QhT1RSIERldiBU 12 | ZWFtIDxvdHJAY3lwaGVycHVua3MuY2E+iEYEEBECAAYFAkGvc7sACgkQyyhygoBySXsh6ACg 13 | 1DGTgtga5e1ci0GBdYV4RoTC3xQAoJkIq/VjnuFtigVmHrBBu2nLW0a+iF4EExECAB4FAkGu 14 | U5kCGwMGCwkIBwMCAxUCAwMWAgECHgECF4AACgkQ3tZOuyuofFwKRgCdHe4ozbAEpC7B67N7 15 | Dro51LJP8W0AoItbsNlIkaCp0bZIVFyrJ+ycLnoZiJwEEAEBAAYFAkGuV2QACgkQRlGJMStI 16 | 9vXhYgP/cEtYz+DBC/FWtI8TYScwZQCZh15l82lUao7fTi98++URArAUl0CyCdRnS9qjiS5a 17 | VAbG6fcbgZyGtBLF/8d4+KrfBJgyLk/I6/n5M1A9zh0E7dXjIcP6ngwD40Jrr+DhV98FirK3 18 | qWbpasWplgh9LqQbmtsd6kjbO5xeF3Wc6/OJAhwEEAECAAYFAk3mCdcACgkQUn55uqO1OZhC 19 | 3A//dRx6enzMg8J1X3x5can77DbjmxF10gk6FstNbPGqp7EyLNo00yUGO0dfGYGQexR7jXEl 20 | jGrpkTj9IFu9CNJDV4g2GL7w7hQ0L3WCUf+MVTBmWLKK3km3qh4qkmlBejg5pSt/3mBNypSE 21 | sugAw1EGlLOdCwUhMLWbPRGCM8XhHG8K33cWUaizxyGUB+XEjNIOWF/NVVAhdasmC5M2Eo5/ 22 | Tx/lD08bQUVDX1o4dcwbADWXtaa6QtucJEAbs9AggiGUOpeOyEcq5NZ5NtmsTYsvvjwBLdkw 23 | gtjMdSfJe3+1oaWsEXuseu14Z7IGL6A6Deu9A/FgabNc1hx9y5XdH+uVe27t/JBmV7/o+5t8 24 | 9DdzQl0Iv9YBZn6CeknAN2UEjvISot01ijIfZsmjARE9Yib0epgIRKKXTFrgeRmNcHYZiiJP 25 | q9kdJyajXvanD+QC55SetFqJwZ/0t4MLv/xAcSx4qR5YdCh240Pb6HO8sDj8egFIsIqwLhQq 26 | 7OuaF+2Wq9tAd1hZvUZNVkHdHtTfJYCUjx50FyvWkz/mtlB90gXIUT+Wk+cojEAW/1kO3CZZ 27 | ICbO7/68p8BUeANa95uH5IfFpJZaqLipwoF+Z/PPKnUbB0rOf8F+3fFETBFM1UfHMTRm8jcw 28 | /kSx/QnJ+k7XSc6C3/rHwoUOd5Q/sCeQA5/hLJg= 29 | =cyu2 30 | -----END PGP PUBLIC KEY BLOCK----- 31 | -------------------------------------------------------------------------------- /win32/fetch-and-verify.sh: -------------------------------------------------------------------------------- 1 | mkdir -p files/ 2 | cd files/ 3 | 4 | # git 5 | wget -c https://msysgit.googlecode.com/files/Git-1.8.4-preview20130916.exe 6 | 7 | # python 8 | wget -c http://www.python.org/ftp/python/2.7.5/python-2.7.5.msi 9 | wget -c http://www.python.org/ftp/python/2.7.5/python-2.7.5.msi.asc 10 | wget -c "https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py" 11 | 12 | # pywin32 13 | wget -c "http://downloads.sourceforge.net/project/pywin32/pywin32/Build%20218/pywin32-218.win32-py2.7.exe?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fpywin32%2Ffiles%2Fpywin32%2FBuild%2520218%2F&use_mirror=iweb" -O pywin32-218.win32-py2.7.exe 14 | 15 | # pyinstaller 16 | wget -c https://github.com/pyinstaller/pyinstaller/tarball/develop -O pyinstaller-git.tar.gz 17 | 18 | # mingw 19 | wget -c "http://downloads.sourceforge.net/project/mingw/Installer/mingw-get-inst/mingw-get-inst-20120426/mingw-get-inst-20120426.exe?r=&use_mirror=superb-dca3" -O mingw-get-inst-20120426.exe 20 | 21 | # openssl 22 | wget -c https://slproweb.com/download/Win32OpenSSL-1_0_1e.exe 23 | 24 | # pycrypto 25 | wget -c "https://pypi.python.org/packages/source/p/pycrypto/pycrypto-2.6.tar.gz#md5=88dad0a270d1fe83a39e0467a66a22bb" -O pycrypto-2.6.tar.gz 26 | wget -c "https://pypi.python.org/packages/source/p/pycrypto/pycrypto-2.6.tar.gz.asc" 27 | 28 | # python pure otr 29 | wget -c https://github.com/afflux/pure-python-otr/archive/1.0.0beta6.zip -O python-potr-1.0.0beta6.zip 30 | 31 | # pidgin 32 | wget -c "http://downloads.sourceforge.net/project/pidgin/Pidgin/2.10.7/pidgin-2.10.7-offline.exe.asc?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fpidgin%2Ffiles%2FPidgin%2F2.10.7%2F&use_mirror=superb-dca3" -O pidgin-2.10.7-offline.exe.asc 33 | wget -c "http://downloads.sourceforge.net/project/pidgin/Pidgin/2.10.7/pidgin-2.10.7-offline.exe?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fpidgin%2Ffiles%2FPidgin%2F2.10.7%2F&use_mirror=superb-dca3" -O pidgin-2.10.7-offline.exe 34 | 35 | # pidgin OTR 36 | wget -c http://www.cypherpunks.ca/otr/binaries/windows/pidgin-otr-4.0.0-1.exe 37 | wget -c http://www.cypherpunks.ca/otr/binaries/windows/pidgin-otr-4.0.0-1.exe.asc 38 | 39 | mkdir -p gpgkeys/ 40 | gpg --homedir gpgkeys/ --import ../*asc 41 | 42 | for sig in `find -iname "*.asc"`; do 43 | echo "Verifying $sig" 44 | gpg --homedir gpgkeys/ --verify $sig 45 | done 46 | -------------------------------------------------------------------------------- /win32/pidgin-windows-pubkeys.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBFBqbQ8BEADJ4spu7liM5w0hPJ6kNcdp9L5lhIDChZgShPRJAZCNwx0RLk+Y 4 | pJF2nTHKn/7d/YZXU9i8n3knmZAkzM+rMe4T4TIBWrvFg3euEDXXEMNUwNQ4Dhak 5 | 4YKwYPh8DbxTQuOEZpy/qSpRXdFlfR6kwdVvywUQR6jcdEoWlDt2MgGXnwY9P+qf 6 | vgS3AJvU6FuuDUH/KEowrqUu3py4bTYppkhxxstctWRPuVeZ9Il4JNRfrVQGTlHE 7 | h8prmKGN9jW+3OiAK5B+hXigYIvBZoqPTzcoyuf1PFXLUVtQHJkMRvljvCtq0ty4 8 | zoiIwliF0rvjmoJwwFZXJYWapWt63zn3g6kId9Puj8PHe7l5MMw/rvrPzX9UEyVY 9 | lNNkqv9fMckqJnfpOhp3pFvBF19y6hkKx5Lpd1qJbUF4Kcg79Cljj2bfzbjB8REq 10 | ZlTAd639yJ0Zsz7QV7B/5cAdvpFnj1gIWi/wFlQW6MK6AxFS9ab9OTu4+qsMyrVk 11 | SxJQcwd5yLTjQcIGDDlBycuDW7c3ekRmYz7flwKn3oKQb3dyloH5jjRamRLu3QZ/ 12 | h3tBMW1uhAgGDqCcJ+1VAt4D4FNeal7bSOyQOJ4Q5eYlOB37SKS8SpXNNoPkjCXk 13 | U8oPnpBumEv17615CB1zNVFe16jePzUHaHUAa6rs2EvTRu1njuckF4HuPwARAQAB 14 | tCNEYW5pZWwgQXRhbGxhaCA8ZGF0YWxsYWhAcGlkZ2luLmltPohGBBARAgAGBQJQ 15 | am/9AAoJEGumGXVpsjMZSKwAnjeJW4chLMMYp54e3umtKBFoJSwEAJ9ta2v29TNB 16 | S+UkDR62A0MWKaKKcYkBHAQQAQIABgUCUcMqzwAKCRB/60XQbNy0IhXiB/0a+fzo 17 | 1GAW9YiuuXDhBV6mCG58Ne1/kHlvsiIrKyCpr62d4D0hM+rX5U9LuZHPRntlEqKN 18 | RIIPve7/arLQIYg16TcNCVaZ/v/p5dZRUw8O9NTWw9wwOqMaPxLN0DVa7FrcwNh9 19 | 72Lh05aJRP210/05w2vVJMEdbNOyIedGxKPRlxcsHomP3XENPBXewIqu3in0BkpW 20 | Y1hilrSFDt7/hkS+dUh8e8bYfcvUtP6uVAhjhOT6XLA4mQiZWt6muyKhu6U/kC0P 21 | tvs7CnbHG7ez8iMoJTYCVHXcJP8VWTEh3dOid5JmbpMwXvuH6wFO/t3Dfj4puh+O 22 | fJivTpRk+Vdx2CwLiQI7BBMBAgAlAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIX 23 | gAUCUGputwIZAQAKCRCGcj/u3okFdM83EAC8PKmuOzLOTHQBmVjA3VU9JMTGdEql 24 | xjDWatWVGNS8vjz8FT1+DtCLIprUFbgcn2bAqDBh3FhyAQ0Nd3rwAwKW1OL2uWmo 25 | veSq6Mazpiu3tspxWcW8AdeNSEIqrK77iFyv7jDX0iZywX3BTiZfbtN8je87KCJk 26 | vRj6/ssKM2Zplt3i5CyhRRphPzmaotLIzXzY3PBthOmYYcMbs1CnVwdI/n7DwpcE 27 | zzTHbwuswcztLSoh9yhj1ksokalbDMWqOvU4UgZ4aTDfuQ427oJBgUl/Q0CFhiJe 28 | BrXMUmnnseBGfgMK4I98mMqRYikRlH44lo6aCTq9+orgSxcYE2pD3yF8W7rzukq+ 29 | JinSYUFEIm5j+GNLIe1T3Jo8eKKs82bMoZIOursq54ZniVntqVZ+Um/G+Nh5kLUZ 30 | SjnU2XHh/oP+JPh64IG5LOe9zb82vIK2/bNl0Fc+h7iBTcxZgUBZsHQPFhKUi1Fx 31 | qvLE57Hv2PbLbptlvt2FDTAYwmgZYuHVZwLZsFv/UNB+F7j/6/bPly3Pujg6emBR 32 | qrtWC3GefGKSdgbuZ16IpEWgtPtDXoehwJe2NDsVlz3J3NWyCPvFnqHfvafQPwGj 33 | WZRvv1Gn7iT3lPIzq5BFO/A6nYqnUbLtWTfNLIGvndyou+etYgBOEDPlWu7uj5pf 34 | Nd2uL0ipS0RxSbQpRGFuaWVsIEF0YWxsYWggPGRhbmllbC5hdGFsbGFoQGdtYWls 35 | LmNvbT6IRgQQEQIABgUCUGpv/QAKCRBrphl1abIzGSOiAJ0SL2NsifMZUB3MLrZt 36 | hQaDq9/TqgCffJIfXuttdgEScIJWxHHYQSTuU0qJARwEEAECAAYFAlHDKs8ACgkQ 37 | f+tF0GzctCJF8gf8CQFzOmuUCRBQtqzw3FQay3XWYmEZxO4sUC0rqXy5HnTktJKH 38 | ZbNh17mNmC9Hq+IlWdhlCzBRS38qkevsBltcCHfQYUAtdCaSFVtVN8zHolt7GbRX 39 | X7/mG3sNdFkPkxwxTW2IYR59ytDeCH/Dygp61vfvObHHdP/+bMSXUkJHnK5Pr+xK 40 | GONUlVjesSbg1BvtA/m0dp7vcgCE8E7f/vkbx7WrwEo3/jKogrPyfvAHMJj5XRlZ 41 | /Cb6KbFZUsVsY4Bb1dBLOX0OwO/t56Q3Ts+070QweNh6rL7HWTvh9Tvac92lTaMr 42 | OQ1lU7TqD0ppQeCYLzFA7knhnmx5y+rz1e8IiYkCOAQTAQIAIgUCUGpuQAIbAwYL 43 | CQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQhnI/7t6JBXTAtg//e9w5WT5VVLLa 44 | pXjDjVt1KoswvJTsbQujZ3+LWr8CaVYzmwpgyvBQlGRuE01xA5QIQUmj55p4qJP5 45 | 9DI63rVmYoyOYaHkQ4SiPBcyLVbsL2L2AUFvqB9KvHh62LXh5nSuSC/bcTA9PCCb 46 | iUe6YChbFgZIgEyJOFG8A0qIoVPVCbmoqNKvT72+8Nk3Ndho92ehWS8aahjTEUl2 47 | +LoNovdmfzlnw5zY/cSTCZoZXeENWOfnQ7Cfy8L3eJi/Ewfhp9swlpmvQCrGJOiU 48 | ZYANHGkk3rKSkuRCYrOPKVEGuH+WZAp7KfRO+1kFAJEhoYK+eHllNyoVwnMSXL2G 49 | J0iyukorWip90wc3dCqDGIFLymU+V0LN81i4y2u7i2OcSgmeT+lXYeYJhxvjMbZu 50 | fJ8Vc8ciWvcFl2q4XYNDdAc2WBsE1fwO1CGcz1lgU9ZoUwkuKu1HUldchEjZm8iu 51 | 4I6Bonri6rMsnNa9VPgnZprRtA+ntckLkyRtpZOc4rk/uHz13UAt27a5wGOa3iy+ 52 | TbOOSXA5Dq5XxZN5eqSuHxK+tbcG8lP2po2FBrd0uZ4X+eC5+NCiqYnEZJiCsLhO 53 | wBuJJ67AZ07jDDY3Ju51w9321fFJOm1YbSBdnQfge4PmLWJ1/7FmwQHqLYBLrc2l 54 | bgkVpRsLXbzUcOl69oblK0o6qf1bbaK0KURhbmllbCBBdGFsbGFoIDxkYW5pZWxf 55 | YXRhbGxhaEB5YWhvby5jb20+iEYEEBECAAYFAlBqb/0ACgkQa6YZdWmyMxkVfgCg 56 | iuI9umRQde5MpEU399FK6Sb16AAAn0JW9fEZ6p14OS4bxQ8E19ArF2wGiQEcBBAB 57 | AgAGBQJRwyrPAAoJEH/rRdBs3LQiabQH+wfhELiER+vj3KLazm+K9tQNgwRuOmBp 58 | ieRxgZg64WNXzaAL+8ERbxzGrX7e0bvi5e4bXyU/tudgMiQULVO4lSmogBMKKvvV 59 | r5KzWR9Zl3jXYdBdVZJouwcguCm85yLipgCz9opTeI6LEuWryJ/+GLNSAv29RMGK 60 | D9a274NqWfknGtCUAWv8E1gFECi+YzKrU77IYvUsBuHNCr39IsWE8GdATo9Afegc 61 | +BIigksGTH3MnDiS8G3IrQxMTrQN0YcXr/4eh7rWPdjbmDyGE7wL6Fchxa0hodpm 62 | OZKSqWaWb6225/huCbStfuWRPTH8zCJJwy2+IIorNqXrvwgAuoJWAvyJAjgEEwEC 63 | ACIFAlBqblwCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEIZyP+7eiQV0 64 | JO4P/35fs1y5/i6VPpMJ75Yp69TXn7z0LEEsh2J3N9ssSVkKNwHUvohYQovZDtcz 65 | scznSdpWcUN+aNf8vQ5j63Ifc4W3N/1fdmltMYXYQr1GhfVlyRE7o1AizzyjIQvq 66 | bKvzku7nC1xnWvhJ9JwDXSeqSnaG5E9qNv23jBSckJD7I7Gg7DkleIegHd+DAUAN 67 | jPSMEZM9LuaV7SNLW+2yNMDj0N46ZTq8X524BRpz/XpL8FP3tgXG3DEsaVlDhTJg 68 | GCB/rw+lkbcpbe40yDJaaojUe1VjUelq4RyiyJBIgTN/uFFbsQeS77C405QsvfFv 69 | eWtHIXhTDyrYCOv1QHDBeACbJBOoK50C37xLt5nO/mrq8CSZg40mp7FgtOXmA9S0 70 | LztbM5WbUPkZNw+G7aFexG1eIo4UgQ60eOGd7TvlJgVpR5fW3WxrINKmELbVrivZ 71 | zsCbFpcV0wFrZKQvGDhqrEqky1p3qDvSNWTTPXtSb2Ip3Mo+PlCODxvFGuuD1ccd 72 | XZi2Zx0nptcr8JW7Nni8SyZFGXNlUvh72W5Whw3OYYHyVx7OgAuATPLPeEMNpsli 73 | PPHS+jgdKEqCewKCf0LbF02KqqdUqM6rNbH3DvnGKnxDTh5trfqFexAJFDzvrC95 74 | aOY/Uc6UKdSQTVwGsR3rU+UbjtVbVaUmK/Cx2ut6VG9e4ea/uQINBFBqbQ8BEACt 75 | oK9RWmFiz0xX4/Ef4f7BfqUY/RMaS5BAlww2hkiaDNODkEtoQcJin3qcMbjczzPA 76 | 36YiRB4uDQYhQcOA6L/GEGVwwrqi4vM05ILCYNU9YfVnLUjzkKx24xWRJMrEASRy 77 | p2mAq3j1P7jCIm3QBnISheZcfW6mQJB7az3Cpbtt9GlrYk9Z1m9EEbNSt0uF70yQ 78 | CwA2LlDhPVsDOGNuYn2ia+ij50QRh/0A2t9FKLQVO5B7QCFcGuRa27xVPP8nRxdy 79 | mvT3L6Me78mtTGso1bGqsTQHHiTdNDiocozUD73qqtPgwQ/evUhJPxGqGCrzaLxP 80 | m8LfpKtIXylmIp/PAXUOQ7Dv1y3El2Xsck0DCxLdb0GiTUeGDGFQX6G0CUuRLqhf 81 | u7VvpQuzKuNw0dZHb3mKaXBCSzvUajM5kWwqMT1SBNsvXKCqGRhFXP5C+c+tMmca 82 | CTB2ES6zpKtKq8zyxT+YfGkeyhObLrA9ul4Wus/gNImUL4lmCDbk8qfI0DUbPQLp 83 | IMn2KS+qSiSnIcf1JJGdSTeprYiQtescY2hasrN7l24BwmOCo7ba2v16P/0n5tVO 84 | wSSMDY0HLE1YmBuKj7T1g71pOumlSLwU7p/Xi1Ae0qHIgmsPfWCwTR+L7S47ZtS/ 85 | P4m+vYVLRtavLEWRJL08Lr6mIkU2FIaczxLDEzrx5QARAQABiQIfBBgBAgAJBQJQ 86 | am0PAhsMAAoJEIZyP+7eiQV0iJ4P/0dR1gN/VS7RvGTn30XlsszfhZCpXYX7Gt1d 87 | QBPITeZQyXTckWpCCxky1xy4xWGBvglUZzpcANJ8aql3V3ARGSmOGIgiJE7OftOj 88 | 7HmsukDr91PREDrFwbsAvSatuiNx5J6GA9tsOm1vMF2Q8E0y46Gh6gdgzHO6eKWL 89 | KjC/wIevDWzRJ5nLKrNwGrubbJp6c7Gp3DtwQgpdN4mxQtZrcwyhuWlzdGu7ZT2I 90 | /AhV65rgSyxLJr4LDBkwaiZy2ypqGhyArkP09hu6BtUNsqAt+dPT8yTV8Gext8kk 91 | tZAaH5RyWAsBgiD2TZRcPobFDYUk/RkNbpA33orAXs68hYfJHXrMhmXzp7/4RLVG 92 | MD4rBLYUFRzaDZR5doLAOEMPZQyzO2Ad6nX5usONrU3Dkn5mZEnNzNCmCpRo2GS2 93 | WrKoDHhIV7ljYm9lFWXKgMS1Buc8ya9p1LmmcKdba6ZqenMcfAss9GtNA9qeDXTw 94 | mMMNRfkpbAhiISuamZdDMHs7VLTnvtBWHIiKs0ojxJt8PUuVzl38JY+A79zbZJn6 95 | cZ451IufJFeK6uQG28trhXZIrDnbi/OXDX4y7a2k5P/kY75CEtZHAFryeYxwbIri 96 | QCmUUmsHX16NEEHk551fqRnfjCTo+Jgn+fYQSYt9JuC+bA9JKh5vqtIQgE+Kbpxb 97 | Un75lhEA 98 | =1DUj 99 | -----END PGP PUBLIC KEY BLOCK----- 100 | 101 | -------------------------------------------------------------------------------- /win32/w32-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guardianproject/keysync/96990dc739a421b6072cc63da94d916e56941acf/win32/w32-screenshot.png --------------------------------------------------------------------------------