├── LICENSE.txt ├── README.md ├── js ├── openpgp.min.js ├── rc_openpgpjs.crypto.js └── rc_openpgpjs.js ├── localization ├── de_DE.inc ├── en_US.inc ├── fr_FR.inc ├── it_IT.inc ├── nl_NL.inc ├── pl_PL.inc └── sv_SE.inc ├── package.xml ├── pygpghttpd ├── DOCS │ └── API.txt ├── LICENSE.txt ├── README.md ├── accepted_domains.txt ├── cert.pem ├── example │ ├── example.htm │ └── js │ │ └── jquery-1.10.2.min.js └── pygpghttpd.py ├── rc_openpgpjs.php ├── skins └── larry │ ├── images │ ├── key_manager.png │ ├── lock.png │ └── openpgp.png │ ├── openpgp.png │ ├── rc_openpgpjs.css │ └── templates │ ├── key_manager.html │ ├── key_search.html │ └── key_select.html └── test └── index.htm /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | THIS PROJECT IS DEAD. 2 | ================ 3 | This project is killed due to lost interest, motivation and resources. Its 4 | future development and survival relies entirely on the FOSS community. Please 5 | turn to maintained forks instead. 6 | 7 | Feel free to contact me if you have any questions. 8 | 9 | rc_openpgpjs 10 | ================ 11 | rc_openpgpjs is an open source (GPLv2) extension adding OpenPGPs functionality 12 | to the Roundcube webmail project. rc_openpgpjs is written with the intention to 13 | be as user friendly as possible for everyday PGP use. See 14 | [Why do you need PGP?][why], [OpenPGP.js][openpgpjs] and [Roundcube][roundcube] 15 | for more info. 16 | 17 | Features 18 | -------- 19 | - E-mail PGP signing 20 | - E-mail PGP encryption and decryption 21 | - Secure key storage (HTML5 local storage) 22 | - Key generation 23 | - Key lookups against PGP Secure Key Servers 24 | 25 | Key storage 26 | ----------- 27 | The keys are stored client side using HTML5 web storage. Private keys are never 28 | transferred from the user's local storage. Private and public keys can be 29 | exported from the web storage and be used outside of Roundcube and equally 30 | externally generated keys can be imported and used inside Roundcube. 31 | 32 | Key lookups 33 | ----------- 34 | Public keys can be imported from PGP Secure Key Servers, i.e. pool.sks-keyservers.net and 35 | any other Public Key Server which follows the [OpenPGP HTTP Keyserver Protocol 36 | (HKP)][draft], i.e pgp.mit.edu. 37 | 38 | Installation 39 | ------------ 40 | 1. Copy plugin to 'plugins' folder 41 | 2. Add 'rc_openpgpjs' to plugins array in your Roundcube config (config/main.inc.php) 42 | 43 | Contact 44 | ------- 45 | For any bug reports or feature requests please refer to the [tracking system][issues]. 46 | 47 | Questions? Please see the [FAQ][faq]. 48 | 49 | [roundcube]: http://www.roundcube.net/ 50 | [openpgpjs]: https://openpgpjs.org/ 51 | [issues]: https://github.com/qnrq/rc_openpgpjs/issues 52 | [why]: http://www.pgpi.org/doc/whypgp/en/ 53 | [draft]: https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 54 | [faq]: https://github.com/qnrq/rc_openpgpjs/wiki/FAQ 55 | -------------------------------------------------------------------------------- /js/rc_openpgpjs.crypto.js: -------------------------------------------------------------------------------- 1 | /* 2 | * +-------------------------------------------------------------------------+ 3 | * | OpenPGP.js implemented in Roundcube. This file covers the cryptographic | 4 | * | functionalities. | 5 | * | | 6 | * | Copyright (C) 2013 Niklas Femerstrand | 7 | * | | 8 | * | This program is free software; you can redistribute it and/or modify | 9 | * | it under the terms of the GNU General Public License version 2 | 10 | * | as published by the Free Software Foundation. | 11 | * | | 12 | * | This program is distributed in the hope that it will be useful, | 13 | * | but WITHOUT ANY WARRANTY; without even the implied warranty of | 14 | * | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 15 | * | GNU General Public License for more details. | 16 | * | | 17 | * | You should have received a copy of the GNU General Public License along | 18 | * | with this program; if not, write to the Free Software Foundation, Inc., | 19 | * | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | 20 | * | | 21 | * +-------------------------------------------------------------------------+ 22 | * */ 23 | 24 | openpgp.init(); 25 | // openpgp.config.debug = true 26 | 27 | /** 28 | * Encrypt (and sign) a message 29 | * 30 | * @param pubkeys {Array} Public keys 31 | * @param text {String} Message to encrypt 32 | * @param sign {Bool} Sign and encrypt the message? 33 | * @param privkey {String} Required if sign is True 34 | * @return {String} Encrypted message 35 | */ 36 | // TODO: Feed key armored and do openpgp.read_* here 37 | function encrypt(pubkeys, text, sign, privkey, passphrase) { 38 | sign = (typeof sign === "undefined") ? 0 : 1; 39 | if(sign) { 40 | privkey = (typeof privkey === "undefined") ? 0 : privkey; 41 | passphrase = (typeof passphrase === "undefined") ? 0 : passphrase; 42 | 43 | if(!privkey) { 44 | alert("missing privkey"); 45 | return false; 46 | } 47 | 48 | if(!passphrase) { 49 | alert("missing passphrase"); 50 | return false; 51 | } 52 | 53 | if (!privkey[0].decryptSecretMPIs(passphrase)) { 54 | alert("Password for secrect key was incorrect!"); 55 | return; 56 | } 57 | 58 | try { 59 | encrypted = openpgp.write_signed_and_encrypted_message(privkey[0], pubkeys, text); 60 | return(encrypted); 61 | } catch (e) { 62 | return false; 63 | } 64 | } 65 | 66 | try { 67 | encrypted = openpgp.write_encrypted_message(pubkeys, text); 68 | return(encrypted); 69 | } catch(e) { 70 | return false; 71 | } 72 | } 73 | 74 | /** 75 | * Generates key pair 76 | * 77 | * @param bits {Integer} Key length in bits 78 | * @param algo {Integer} Key algorithm type. Currently unused and set to 1 (RSA) 79 | * @param ident {String} Key identity formatted as "Firstname Lastname " 80 | * @param passphrase {String} Passphrase of private key 81 | * @return {Array} Armored key pair 82 | */ 83 | function generateKeys(bits, algo, ident, passphrase) { 84 | try { 85 | keys = openpgp.generate_key_pair(1, bits, ident, passphrase); 86 | arr = new Array(); 87 | arr["private"] = keys.privateKeyArmored; 88 | arr["public"] = keys.publicKeyArmored; 89 | return(arr); 90 | } catch(e) { 91 | return false; 92 | } 93 | } 94 | 95 | /** 96 | * Sign a meesage 97 | * 98 | * @param msg {String} Message to sign 99 | * @param privkey_armored {String} Armored private key to sign message 100 | * @param passphrase {String} Passphrase of private key 101 | * @return {String} Signed message 102 | */ 103 | function sign(msg, privkey_armored, passphrase) { 104 | var priv_key = openpgp.read_privateKey(privkey_armored); 105 | 106 | if(!priv_key[0].decryptSecretMPIs(passphrase)) { 107 | alert("WRONG PASS"); 108 | } 109 | 110 | try { 111 | var signed = openpgp.write_signed_message(priv_key[0], msg); 112 | return(signed); 113 | } catch(e) { 114 | return false; 115 | } 116 | } 117 | 118 | /** 119 | * Decrypt a meesage 120 | * 121 | * @param msg {String} Message to decrypt 122 | * @param privkey_armored {String} Armored private key to decrypt message 123 | * @param passphrase {String} Passphrase of private key 124 | * @return {String} Decrypted message 125 | */ 126 | function decrypt(msg, privkey_armored, passphrase) { 127 | if(!("decrypt" in msg[0])) { 128 | return false; 129 | } 130 | 131 | var priv_key = openpgp.read_privateKey(privkey_armored); 132 | var keymat = null; 133 | var sesskey = null; 134 | 135 | if(!priv_key[0].decryptSecretMPIs(passphrase)) { 136 | alert("wrong pass"); 137 | return false; 138 | } 139 | 140 | for (var i = 0; i< msg[0].sessionKeys.length; i++) { 141 | if (priv_key[0].privateKeyPacket.publicKey.getKeyId() === msg[0].sessionKeys[i].keyId.bytes) { 142 | keymat = { key: priv_key[0], keymaterial: priv_key[0].privateKeyPacket}; 143 | sesskey = msg[0].sessionKeys[i]; 144 | break; 145 | } 146 | 147 | for (var j = 0; j < priv_key[0].subKeys.length; j++) { 148 | if (priv_key[0].subKeys[j].publicKey.getKeyId() === msg[0].sessionKeys[i].keyId.bytes) { 149 | if(!priv_key[0].subKeys[j].decryptSecretMPIs(passphrase)) { 150 | alert("Wrong pass"); 151 | return false; 152 | } 153 | keymat = { key: priv_key[0], keymaterial: priv_key[0].subKeys[j]}; 154 | sesskey = msg[0].sessionKeys[i]; 155 | break; 156 | } 157 | } 158 | } 159 | 160 | try { 161 | decrypted = msg[0].decrypt(keymat, sesskey); 162 | return decrypted; 163 | } catch (e) { 164 | return false; 165 | } 166 | } 167 | 168 | 169 | /** 170 | * Verify signature of a clear-text message 171 | * 172 | * @param msg {array} Message to verify 173 | * @param pubkeys {array} Public key(s) to verify against 174 | */ 175 | function verify(msg, pubkeys) { 176 | return msg[0].verifySignature(pubkeys); 177 | } 178 | 179 | 180 | function parseMsg(msg) { 181 | return openpgp.read_message(msg); 182 | } 183 | 184 | function hasPrivateKey() { 185 | return openpgp.keyring.hasPrivateKey(); 186 | } 187 | 188 | function getPrivkeyCount() { 189 | return openpgp.keyring.privateKeys.length; 190 | } 191 | 192 | function getPubkeyCount() { 193 | return openpgp.keyring.publicKeys.length; 194 | } 195 | 196 | function getFingerprint(i, private, niceformat) { 197 | if(typeof(private) == "undefined") { 198 | private = false; 199 | } 200 | 201 | if(typeof(niceformat) == "undefined") { 202 | niceformat = true; 203 | } 204 | 205 | if(private == false) { 206 | fingerprint = util.hexstrdump(openpgp.keyring.publicKeys[i].obj.getFingerprint()).toUpperCase(); 207 | } else { 208 | fingerprint = util.hexstrdump(openpgp.keyring.privateKeys[i].obj.getFingerprint()).toUpperCase(); 209 | } 210 | 211 | if(niceformat) { 212 | fingerprint = fingerprint.replace(/(.{2})/g, "$1 "); 213 | } else { 214 | fingerprint = "0x" + fingerprint.substring(0, 8); 215 | } 216 | 217 | return fingerprint; 218 | } 219 | 220 | function getKeyID(i, private) { 221 | if(typeof(private) == "undefined") { 222 | private = false; 223 | } 224 | 225 | if(private == false) { 226 | key_id = "0x" + util.hexstrdump(openpgp.keyring.publicKeys[i].obj.getKeyId()).toUpperCase().substring(8); 227 | } else { 228 | key_id = "0x" + util.hexstrdump(openpgp.keyring.privateKeys[i].obj.getKeyId()).toUpperCase().substring(8); 229 | } 230 | 231 | return key_id; 232 | } 233 | 234 | function getPerson(i, j, private) { 235 | if(typeof(private) == "undefined") { 236 | private = false; 237 | } 238 | 239 | if(private == false) { 240 | person = openpgp.keyring.publicKeys[i].obj.userIds[j].text; 241 | } else { 242 | person = openpgp.keyring.privateKeys[i].obj.userIds[j].text; 243 | } 244 | 245 | return person; 246 | } 247 | 248 | function getPubkeyForAddress(address) { 249 | var pubkey = openpgp.keyring.getPublicKeyForAddress(address); 250 | return pubkey; 251 | } 252 | 253 | function getFingerprintForSender(sender) { 254 | var pubkey = getPubkeyForAddress(sender); 255 | var fingerprint = util.hexstrdump(pubkey[0].obj.getFingerprint()).toUpperCase().substring(8).replace(/(.{2})/g,"$1 "); 256 | return fingerprint; 257 | } 258 | 259 | function getPrivkeyArmored(id) { 260 | var keyid = openpgp.keyring.privateKeys[id].obj.getKeyId(); 261 | var privkey_armored = openpgp.keyring.getPrivateKeyForKeyId(keyid)[0].key.armored; 262 | return privkey_armored; 263 | } 264 | 265 | function getPrivkeyObj(id) { 266 | var privkey_armored = getPrivkeyArmored(id); 267 | return privkey = openpgp.read_privateKey(privkey_armored); 268 | } 269 | 270 | // Gets privkey obj from armored 271 | function getPrivkey(armored) { 272 | var privkey = openpgp.read_privateKey(armored); 273 | return privkey; 274 | } 275 | 276 | function decryptSecretMPIs(i, p) { 277 | return openpgp.keyring.privateKeys[i].obj.decryptSecretMPIs(p); 278 | } 279 | 280 | function decryptSecretMPIsForId(id, passphrase) { 281 | var keyid = openpgp.keyring.privateKeys[id].obj.getKeyId(); 282 | var privkey_armored = openpgp.keyring.getPrivateKeyForKeyId(keyid)[0].key.armored; 283 | var privkey = getPrivkey(privkey_armored); 284 | return privkey[0].decryptSecretMPIs(passphrase); 285 | } 286 | 287 | function importPubkey(key) { 288 | try { 289 | openpgp.keyring.importPublicKey(key); 290 | openpgp.keyring.store(); 291 | } catch(e) { 292 | console.log(e); 293 | return false; 294 | } 295 | return true; 296 | } 297 | 298 | function importPrivkey(key, passphrase) { 299 | try { 300 | openpgp.keyring.importPrivateKey(key, passphrase); 301 | openpgp.keyring.store(); 302 | } catch(e) { 303 | return false; 304 | } 305 | 306 | return true; 307 | } 308 | 309 | function parsePrivkey(key) { 310 | try { 311 | return openpgp.read_privateKey(key)[0]; 312 | } catch(e) { 313 | return false; 314 | } 315 | } 316 | 317 | function removeKey(i, private) { 318 | if(typeof(private) == "undefined") { 319 | private = false; 320 | } 321 | 322 | if(private) { 323 | return openpgp.keyring.removePrivateKey(i); 324 | } 325 | 326 | return openpgp.keyring.removePublicKey(i); 327 | } 328 | 329 | function verifyBasicSignatures(i) { 330 | return (openpgp.keyring.publicKeys[i].obj.verifyBasicSignatures() ? true : false); 331 | } 332 | 333 | /** 334 | * Extract the algorithm string from a key and return the algorithm type. 335 | * 336 | * @param i {Integer} Key id in keyring 337 | * @return {String} Algorithm type 338 | */ 339 | 340 | function getAlgorithmString(i, private) { 341 | if(typeof(private) == "undefined") { 342 | private = false; 343 | } 344 | 345 | if(private) { 346 | key = openpgp.keyring.privateKeys[i].obj; 347 | } else { 348 | key = openpgp.keyring.publicKeys[i].obj; 349 | } 350 | 351 | if(typeof(key.publicKeyPacket) !== "undefined") { 352 | var result = key.publicKeyPacket.MPIs[0].mpiByteLength * 8 + "/"; 353 | var sw = key.publicKeyPacket.publicKeyAlgorithm; 354 | } else { 355 | // For some reason publicKeyAlgorithm doesn't work directly on the privatekeyPacket, heh 356 | var result = (key.privateKeyPacket.publicKey.MPIs[0].mpiByteLength * 8 + "/"); 357 | var sw = key.privateKeyPacket.publicKey.publicKeyAlgorithm; 358 | } 359 | 360 | result += typeToStr(sw); 361 | return result; 362 | } 363 | 364 | function exportArmored(i, private) { 365 | if(typeof(private) == "undefined") { 366 | private = false; 367 | } 368 | 369 | if(private) { 370 | return openpgp.keyring.privateKeys[i].armored; 371 | } else { 372 | return openpgp.keyring.publicKeys[i].armored; 373 | } 374 | } 375 | 376 | function getKeyUserids(i, private) { 377 | if(typeof(private) == "undefined") { 378 | private = false; 379 | } 380 | 381 | if(private) { 382 | return openpgp.keyring.privateKeys[i].obj.userIds; 383 | } else { 384 | return openpgp.keyring.publicKeys[i].obj.userIds; 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /js/rc_openpgpjs.js: -------------------------------------------------------------------------------- 1 | /* 2 | +-------------------------------------------------------------------------+ 3 | | OpenPGP.js implemented in Roundcube. | 4 | | | 5 | | Copyright (C) Niklas Femerstrand | 6 | | | 7 | | This program is free software; you can redistribute it and/or modify | 8 | | it under the terms of the GNU General Public License version 2 | 9 | | as published by the Free Software Foundation. | 10 | | | 11 | | This program is distributed in the hope that it will be useful, | 12 | | but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 14 | | GNU General Public License for more details. | 15 | | | 16 | | You should have received a copy of the GNU General Public License along | 17 | | with this program; if not, write to the Free Software Foundation, Inc., | 18 | | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | 19 | | | 20 | +-------------------------------------------------------------------------+ 21 | */ 22 | 23 | var VERSTR = "20131021"; 24 | 25 | if(window.rcmail) { 26 | rcmail.addEventListener("init", function() { 27 | if(!window.crypto || !window.crypto.getRandomValues) { // OpenPGP.js specific 28 | rcmail.display_message(rcmail.gettext("no_window_crypto", "rc_openpgpjs"), "error"); 29 | } 30 | 31 | this.passphrase = ""; 32 | rcmail.addEventListener("plugin.pks_search", pks_search_callback); 33 | 34 | if(sessionStorage.length > 0) { 35 | this.passphrase = sessionStorage[0]; 36 | } 37 | 38 | $("#openpgpjs_key_select").dialog({ 39 | modal: true, 40 | autoOpen: false, 41 | title: rcmail.gettext("select_key", "rc_openpgpjs"), 42 | width: "30%", 43 | open: function() { 44 | updateKeySelector(); 45 | }, 46 | close: function() { 47 | $("#selected_key_passphrase").val(""); 48 | $("#openpgpjs_rememberpass").attr("checked", false); 49 | }, 50 | }); 51 | 52 | $("#openpgpjs_key_search").dialog({ 53 | modal: true, 54 | autoOpen: false, 55 | title: rcmail.gettext("key_search", "rc_openpgpjs"), 56 | width: "60%", 57 | open: function() { 58 | $("#openpgpjs_search_results").html(""); 59 | $("#openpgpjs_search_input").val(""); 60 | } 61 | }); 62 | 63 | $("#openpgpjs_key_manager").dialog({ 64 | modal: true, 65 | autoOpen: false, 66 | title: rcmail.gettext("key_manager", "rc_openpgpjs"), 67 | width: "90%", 68 | open: function() { 69 | updateKeyManager(); 70 | } 71 | }); 72 | 73 | $("#openpgpjs_tabs").tabs(); 74 | 75 | // register open key manager command 76 | rcmail.register_command("open-key-manager", function() { 77 | $("#openpgpjs_key_manager").dialog("open"); 78 | }); 79 | rcmail.enable_command("open-key-manager", true); 80 | 81 | if(rcmail.env.action === "compose") { 82 | rcmail.addEventListener("change_identity", function() { 83 | sessionStorage.clear(); 84 | this.passphrase = ""; 85 | }); 86 | // Disable draft autosave and prompt user when saving plaintext message as draft 87 | rcmail.env.draft_autosave = 0; 88 | rcmail.addEventListener("beforesavedraft", function() { 89 | if($("#openpgpjs_encrypt").is(":checked")) { 90 | if(!confirm(rcmail.gettext("save_draft_confirm", "rc_openpgpjs"))) { 91 | return false; 92 | } 93 | } 94 | 95 | return true; 96 | }); 97 | 98 | rcmail.env.compose_commands.push("open-key-manager"); 99 | rcmail.addEventListener("beforesend", function(e) { 100 | if(!beforeSend()) { 101 | return false; 102 | } 103 | }); 104 | } else if(rcmail.env.action === "show" || rcmail.env.action === "preview") { 105 | processReceived(); 106 | } 107 | }); 108 | 109 | /** 110 | * Processes received messages 111 | */ 112 | function processReceived() { 113 | var msg = parseMsg($("#messagebody div.message-part pre").html()); 114 | 115 | // OpenPGP failed parsing the message, no action required. 116 | if(!msg) { 117 | return; 118 | } 119 | 120 | // msg[0].type: 2 == signed only 121 | // msg[0].type: 3 == encrypted only 122 | 123 | showKeyInfo(msg); 124 | 125 | if(msg[0].type === 2) { 126 | // rcmail.env.sender contains "Jon Doe " or just "jd@example.com"; 127 | // We try to extract the email address (according to RFC 5322) in either case 128 | var senderAddress = rcmail.env.sender.match(/[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+/); 129 | if(!senderAddress || !senderAddress.length) { 130 | // In the case of a bogus sender name/address, throw an error 131 | displayUserMessage(rcmail.gettext('signature_invalid_sender', 'rc_openpgpjs'), 'notice'); 132 | return false; 133 | } 134 | senderAddress = senderAddress[0]; 135 | var pubkeys = getPubkeyForAddress(senderAddress); 136 | if(!pubkeys.length) { 137 | displayUserMessage(rcmail.gettext('signature_invalid_no_pubkey', 'rc_openpgpjs') + senderAddress, 'notice'); 138 | return false; 139 | } 140 | if(verify(msg, pubkeys)) { 141 | displayUserMessage(rcmail.gettext('signature_valid', 'rc_openpgpjs') + ': ' + senderAddress, 'confirmation'); 142 | $("#messagebody div.message-part pre").html("********* *BEGIN SIGNED PART* *********\n" + escapeHtml(msg[0].text) + "\n********** *END SIGNED PART* **********"); 143 | return true; 144 | } else { 145 | displayUserMessage(rcmail.gettext('signature_invalid', 'rc_openpgpjs'), 'error'); 146 | return false; 147 | } 148 | } 149 | 150 | if(!getPrivkeyCount()) { 151 | rcmail.display_message(rcmail.gettext("no_key_imported", "rc_openpgpjs"), "error"); 152 | return false; 153 | } 154 | 155 | if((typeof this.passphrase == "undefined" || this.passphrase === "") && getPrivkeyCount() > 0) { 156 | $("#openpgpjs_key_select").dialog("open"); 157 | return false; 158 | } 159 | 160 | // json string from set_passphrase, obj.id = privkey id, obj.passphrase = privkey passphrase 161 | var passobj = JSON.parse(this.passphrase); 162 | var privkey_armored = getPrivkeyArmored(passobj.id); 163 | 164 | decrypted = decrypt(msg, privkey_armored, passobj.passphrase); 165 | if(decrypted) { 166 | $("#messagebody div.message-part pre").html("********* *BEGIN ENCRYPTED or SIGNED PART* *********\n" + escapeHtml(decrypted) + "\n********** *END ENCRYPTED or SIGNED PART* **********"); 167 | } else { 168 | alert("This message was not meant for the private key that you are using."); 169 | } 170 | 171 | return true; 172 | } 173 | 174 | /** 175 | * Extracts public key info from parsed OpenPGP message. 176 | * 177 | * @param string Parsed OpenPGP message 178 | */ 179 | function showKeyInfo(msg) { 180 | var sender = rcmail.env.sender.match(/[a-zA-Z0-9\._%+-]+@[a-zA-Z0-9\._%+-]+\.[a-zA-Z]{2,4}/g)[0]; 181 | 182 | try { 183 | var fingerprint = getFingerprintForSender(sender); 184 | } catch(e) { 185 | return false; 186 | } 187 | 188 | if(typeof(this.getinfo) === "undefined") { 189 | $(".headers-table").css( "float", "left" ); 190 | $(".headers-table").after("
"); 191 | 192 | // Carefully escape anything that is appended to the info table, otherwise 193 | // anyone clever enough to write arbitrary data to their pubkey has a clear 194 | // exploitation path. 195 | $("#openpgpjs_info table tbody").append("Key algo:" + typeToStr(msg[0].type) + ""); 196 | $("#openpgpjs_info table tbody").append("Created:" + escapeHtml(String(msg[0].messagePacket.creationTime)) + ""); 197 | $("#openpgpjs_info table tbody").append("Fingerprint:" + fingerprint + ""); 198 | this.getinfo = false; 199 | } 200 | } 201 | 202 | /** 203 | * Generates an OpenPGP key pair by calling the necessary crypto 204 | * functions from openpgp.js and shows them to the user 205 | * 206 | * @param bits {Integer} Number of bits for the key creation 207 | * @param algo {Integer} To indicate what type of key to make. RSA is 1 208 | */ 209 | function generate_keypair(bits, algo) { 210 | if($("#gen_passphrase").val() === "") { 211 | $("#generate_key_error").removeClass("hidden"); 212 | $("#generate_key_error p").html(rcmail.gettext("enter_pass", "rc_openpgpjs")); 213 | return false; 214 | } else if($("#gen_passphrase").val() !== $("#gen_passphrase_verify").val()) { 215 | $("#generate_key_error").removeClass("hidden"); 216 | $("#generate_key_error p").html(rcmail.gettext("pass_mismatch", "rc_openpgpjs")); 217 | return false; 218 | } 219 | 220 | // TODO Currently only RSA is supported, fix this when OpenPGP.js implements ElGamal & DSA 221 | var ident = $("#gen_ident option:selected").text(); 222 | var keys = generateKeys(bits, 1, ident, $("#gen_passphrase").val()); 223 | $("#generated_keys").html("
" + keys["private"] + "
" + keys["public"]  +  "
"); 224 | $("#generate_key_error").addClass("hidden"); 225 | $("#import_button").removeClass("hidden"); 226 | 227 | return true; 228 | } 229 | 230 | /** 231 | * Import generated key pair. 232 | */ 233 | function importGenerated() { 234 | $("#import_button").addClass("hidden"); 235 | 236 | if(importPrivKey($("#generated_private").html(), $("#gen_passphrase").val())) { 237 | alert(rcmail.gettext("import_gen", "rc_openpgpjs")); 238 | } 239 | 240 | $("#gen_passphrase").val(""); 241 | $("#gen_passphrase_verify").val(""); 242 | } 243 | 244 | /** 245 | * Set passphrase. 246 | * 247 | * @param i {Integer} Used as openpgp.keyring[private|public]Keys[i] 248 | * @param p {String} The passphrase 249 | */ 250 | // TODO: move passphrase checks from old decrypt() to here 251 | function set_passphrase(i, p) { 252 | if(i === "-1") { 253 | $("#key_select_error").removeClass("hidden"); 254 | $("#key_select_error p").html(rcmail.gettext("select_key", "rc_openpgpjs")); 255 | return false; 256 | } 257 | 258 | if(!decryptSecretMPIs(i, p)) { 259 | $("#key_select_error").removeClass("hidden"); 260 | $("#key_select_error p").html(rcmail.gettext("incorrect_pass", "rc_openpgpjs")); 261 | return false; 262 | } 263 | 264 | this.passphrase = JSON.stringify({ "id" : i, "passphrase" : p } ); 265 | processReceived(); 266 | 267 | if($("#openpgpjs_rememberpass").is(":checked")) { 268 | sessionStorage.setItem(i, this.passphrase); 269 | } 270 | 271 | $("#key_select_error").addClass("hidden"); 272 | $("#openpgpjs_key_select").dialog("close"); 273 | 274 | // This is required when sending emails and private keys are required for 275 | // sending an email (when signing a message). These lines makes the client 276 | // jump right back into beforeSend() allowing key sign and message send to 277 | // be made as soon as the passphrase is correct and available. 278 | if(typeof(this.sendmail) !== "undefined") { 279 | rcmail.command("send", this); 280 | } 281 | } 282 | 283 | function fetchRecipientPubkeys() { 284 | var pubkeys = new Array(); 285 | 286 | var c = 0; 287 | var recipients = []; 288 | var matches = ""; 289 | var fields = ["_to", "_cc", "_bcc"]; 290 | var re = /[a-zA-Z0-9\._%+-]+@[a-zA-Z0-9\._%+-]+\.[a-zA-Z]{2,4}/g; 291 | 292 | for(field in fields) { 293 | matches = $("#" + fields[field]).val().match(re); 294 | 295 | for(key in matches) { 296 | recipients[c] = matches[key]; 297 | c++; 298 | } 299 | } 300 | 301 | for (var i = 0; i < recipients.length; i++) { 302 | var recipient = recipients[i].replace(/(.+?<)/, "").replace(/>/, ""); 303 | var pubkey = getPubkeyForAddress(recipient); 304 | if(typeof(pubkey[0]) != "undefined") { 305 | pubkeys.push(pubkey[0].obj); 306 | } else { 307 | // Querying PKS for recipient pubkey 308 | if(confirm(rcmail.gettext("missing_recipient_pubkey", "rc_openpgpjs") + recipient)) { 309 | rcmail.http_post("plugin.pks_search", "search=" + recipient + "&op=index"); 310 | $("#openpgpjs_search_input").attr("disabled", "disabled"); 311 | $("#openpgpjs_search_submit").attr("disabled", "disabled"); 312 | $("#openpgpjs_key_search").dialog("open"); 313 | } 314 | return false; 315 | } 316 | } 317 | 318 | return pubkeys; 319 | } 320 | 321 | /** 322 | * Get the user's public key 323 | */ 324 | function fetchSendersPubkey(armored) { 325 | 326 | if (typeof(armored) == "undefined") { 327 | armored = false; 328 | } 329 | 330 | var re = /[a-zA-Z0-9\._%+-]+@[a-zA-Z0-9\._%+-]+\.[a-zA-Z]{2,4}/g; 331 | var address = $("#_from>option:selected").html().match(re); 332 | 333 | if (address.length > 0) { 334 | var pubkey = getPubkeyForAddress(address[0]); 335 | 336 | if(typeof(pubkey[0]) != "undefined") { 337 | if (armored) 338 | return pubkey[0].armored; 339 | else 340 | return pubkey[0].obj; 341 | } 342 | } 343 | return false; 344 | } 345 | 346 | /** 347 | * Processes messages before sending 348 | */ 349 | function beforeSend() { 350 | if( !$("#openpgpjs_encrypt").is(":checked") && 351 | !$("#openpgpjs_sign").is(":checked")) { 352 | 353 | if ($("#openpgpjs_warn").val() == "1" ) { 354 | if(confirm(rcmail.gettext("continue_unencrypted", "rc_openpgpjs"))) { 355 | // remove the public key attachment since we don't sign nor encrypt the message 356 | removePublicKeyAttachment(); 357 | return true; 358 | } else { 359 | return false; 360 | } 361 | } 362 | else 363 | { 364 | return true 365 | } 366 | } 367 | 368 | if(typeof(this.finished_treating) !== "undefined") { 369 | return true; 370 | } 371 | 372 | // send the user's public key to the server so it can be sent as attachment 373 | var pubkey_sender = fetchSendersPubkey(true); 374 | if (pubkey_sender) { 375 | var lock = rcmail.set_busy(true, 'loading'); 376 | rcmail.http_post('plugin.pubkey_save', { _pubkey: pubkey_sender }, lock); 377 | } 378 | // end send user's public key to the server 379 | 380 | // Encrypt and sign 381 | if($("#openpgpjs_encrypt").is(":checked") && $("#openpgpjs_sign").is(":checked")) { 382 | // get the private key 383 | if((typeof this.passphrase == "undefined" || this.passphrase === "") && getPrivkeyCount() > 0) { 384 | this.sendmail = true; // Global var to notify set_passphrase 385 | $("#openpgpjs_key_select").dialog("open"); 386 | return false; 387 | } 388 | 389 | if(!getPrivkeyCount()) { 390 | alert(rcmail.gettext("no_keys", "rc_openpgpjs")); 391 | return false; 392 | } 393 | 394 | var passobj = JSON.parse(this.passphrase); 395 | var privkey = getPrivkeyObj(passobj.id); 396 | 397 | if(!privkey[0].decryptSecretMPIs(passobj.passphrase)) { 398 | alert(rcmail.gettext("incorrect_pass", "rc_openpgpjs")); 399 | } 400 | // we now have the private key (for signing) 401 | 402 | // get the public key 403 | var pubkeys = fetchRecipientPubkeys(); 404 | if(pubkeys.length === 0) { 405 | return false; 406 | } 407 | // done public keys 408 | 409 | // add the user's public key 410 | var pubkey_sender = fetchSendersPubkey(); 411 | if (pubkey_sender) { 412 | pubkeys.push(pubkey_sender); 413 | } else { 414 | if (!confirm("Couldn't find your public key. You will not be able to decrypt this message. Continue?")) { 415 | return false; 416 | } 417 | } 418 | // end add user's public key 419 | 420 | var text = $("textarea#composebody").val(); 421 | var encrypted = encrypt(pubkeys, text, 1, privkey, passobj.passphrase); 422 | 423 | if(encrypted) { 424 | $("textarea#composebody").val(encrypted); 425 | this.finished_treating = 1; 426 | return true; 427 | } 428 | } 429 | 430 | // Encrypt only 431 | if($("#openpgpjs_encrypt").is(":checked") && 432 | !$("#openpgpjs_sign").is(":checked")) { 433 | // Fetch recipient pubkeys 434 | var pubkeys = fetchRecipientPubkeys(); 435 | if(pubkeys.length === 0) { 436 | return false; 437 | } 438 | 439 | // add the user's public key 440 | var pubkey_sender = fetchSendersPubkey(); 441 | if (pubkey_sender) { 442 | pubkeys.push(pubkey_sender); 443 | } else { 444 | if (!confirm("Couldn't find your public key. You will not be able to decrypt this message. Continue?")) { 445 | return false; 446 | } 447 | } 448 | // end add user's public key 449 | 450 | var text = $("textarea#composebody").val(); 451 | var encrypted = encrypt(pubkeys, text); 452 | if(encrypted) { 453 | $("textarea#composebody").val(encrypted); 454 | this.finished_treating = 1; 455 | return true; 456 | } 457 | } 458 | 459 | // Sign only 460 | if($("#openpgpjs_sign").is(":checked") && 461 | !$("#openpgpjs_encrypt").is(":checked")) { 462 | 463 | if((typeof this.passphrase == "undefined" || this.passphrase === "") && getPrivkeyCount() > 0) { 464 | this.sendmail = true; // Global var to notify set_passphrase 465 | $("#openpgpjs_key_select").dialog("open"); 466 | return false; 467 | } 468 | 469 | if(!getPrivkeyCount()) { 470 | alert(rcmail.gettext("no_keys", "rc_openpgpjs")); 471 | return false; 472 | } 473 | 474 | var passobj = JSON.parse(this.passphrase); 475 | var privkey = getPrivkeyObj(passobj.id); 476 | 477 | if(!privkey[0].decryptSecretMPIs(passobj.passphrase)) { 478 | alert(rcmail.gettext("incorrect_pass", "rc_openpgpjs")); 479 | } 480 | 481 | var privkey_armored = getPrivkeyArmored(passobj.id); 482 | signed = sign($("textarea#composebody").val(), privkey_armored, passobj.passphrase); 483 | 484 | if(signed) { 485 | $("textarea#composebody").val(signed); 486 | return true; 487 | } 488 | 489 | return false; 490 | } 491 | 492 | return false; 493 | } 494 | 495 | /** 496 | * Removes the public key attachment 497 | * Used if the user doesn't sign nor encrypt the message 498 | */ 499 | function removePublicKeyAttachment() { 500 | $("#attachment-list").each(function() { 501 | $(this).find('li').each(function() { 502 | if ($(this).text().indexOf('pubkey.asc') >= 0) { 503 | rcmail.command('remove-attachment', $(this).attr('id')); 504 | return false; 505 | } 506 | }); 507 | }); 508 | } 509 | 510 | function importFromSKS(id) { 511 | rcmail.http_post("plugin.pks_search", "search=" + id + "&op=get"); 512 | return; 513 | } 514 | 515 | /** 516 | * Imports armored public key into the key manager 517 | * 518 | * @param key {String} The armored public key 519 | * @return {Bool} Import successful 520 | */ 521 | function importPubKey(key) { 522 | try { 523 | importPubkey(key); 524 | updateKeyManager(); 525 | $("#importPubkeyField").val(""); 526 | $("#import_pub_error").addClass("hidden"); 527 | } catch(e) { 528 | $("#import_pub_error").removeClass("hidden"); 529 | $("#import_pub_error p").html(rcmail.gettext("import_failed", "rc_openpgpjs")); 530 | alert(rcmail.gettext("import_fail", "rc_openpgpjs")); 531 | alert(e); 532 | return false; 533 | } 534 | 535 | return true; 536 | } 537 | 538 | /** 539 | * op: (get|index|vindex) string operation to perform 540 | * search: string phrase to pass to HKP 541 | * 542 | * To retrieve all matching keys: pubkey_search("foo@bar", "index") 543 | * To retrieve armored key of specific id: pubkey_search("0xF00", "get") 544 | * 545 | * If op is get then search should be either 32-bit or 64-bit. See 546 | * http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00#section-3.1.1.1 547 | * for more details. 548 | * 549 | */ 550 | // TODO: Version 3 fingerprint search 551 | function pubkey_search(search, op) { 552 | if(search.length === 0) { 553 | return false; 554 | } 555 | 556 | rcmail.http_post("plugin.pks_search", "search=" + search + "&op=" + op); 557 | return true; 558 | } 559 | 560 | function pks_search_callback(response) { 561 | $("#openpgpjs_search_input").removeAttr("disabled"); 562 | $("#openpgpjs_search_submit").removeAttr("disabled"); 563 | 564 | if(response.message === "ERR: Missing param") { 565 | console.log("Missing param"); 566 | return false; 567 | } 568 | 569 | if(response.message === "ERR: Invalid operation") { 570 | console.log("Invalid operation"); 571 | return false; 572 | } 573 | 574 | if(response.message === "ERR: No keys found") { 575 | alert(rcmail.gettext("search_no_keys", "rc_openpgpjs")); 576 | return false; 577 | } 578 | 579 | if(response.op === "index") { 580 | try { 581 | result = JSON.parse(response.message); 582 | } catch(e) { 583 | alert(rcmail.gettext("search_no_keys", "rc_openpgpjs")); 584 | return false; 585 | } 586 | 587 | $("#openpgpjs_search_results").html(""); 588 | for(var i = 0; i < result.length; i++) { 589 | $("#openpgpjs_search_results").append("Import" + result[i][0] + "" + "" + result[i][1] + ""); 590 | } 591 | } else if(response.op === "get") { 592 | k = JSON.parse(response.message); 593 | $("#importPubkeyField").val(k[0]); 594 | if(importPubKey($("#importPubkeyField").val())) { 595 | alert(rcmail.gettext("pubkey_import_success", "rc_openpgpjs")); 596 | } 597 | } 598 | } 599 | 600 | /** 601 | * Imports armored private key into the key manager 602 | * 603 | * @param key {String} The armored private key 604 | * @param passphrase {String} The corresponding passphrase 605 | * @return {Bool} Import successful 606 | */ 607 | function importPrivKey(key, passphrase) { 608 | if(passphrase === "") { 609 | $("#import_priv_error").removeClass("hidden"); 610 | $("#import_priv_error p").html(rcmail.gettext("enter_pass", "rc_openpgpjs")); 611 | return false; 612 | } 613 | 614 | try { 615 | privkey_obj = parsePrivkey(key); 616 | } catch(e) { 617 | $("#import_priv_error").removeClass("hidden"); 618 | $("#import_priv_error p").html(rcmail.gettext("import_failed", "rc_openpgpjs")); 619 | return false; 620 | } 621 | 622 | if(!privkey_obj.decryptSecretMPIs(passphrase)) { 623 | $("#import_priv_error").removeClass("hidden"); 624 | $("#import_priv_error p").html(rcmail.gettext("incorrect_pass", "rc_openpgpjs")); 625 | return false; 626 | } 627 | 628 | // Extract pubkey from privkey and import 629 | pubkey = privkey_obj.extractPublicKey(); 630 | importPubkey(pubkey); 631 | importPrivkey(key, passphrase); 632 | updateKeyManager(); 633 | $("#importPrivkeyField").val(""); 634 | $("#passphrase").val(""); 635 | $("#import_priv_error").addClass("hidden"); 636 | 637 | return true; 638 | } 639 | 640 | /** 641 | * Select a private key. 642 | * 643 | * @param i {Integer} Used as openpgp.keyring[private|public]Keys[i] 644 | */ 645 | function select_key(i) { 646 | fingerprint = getFingerprint(i, true, false); 647 | $("#openpgpjs_selected").html("" + rcmail.gettext("selected", "rc_openpgpjs") + ": " + $(".clickme#" + fingerprint).html()); 648 | $("#openpgpjs_selected_id").val(i); 649 | $("#passphrase").val(""); 650 | } 651 | 652 | /** 653 | * Update key selector dialog. 654 | */ 655 | function updateKeySelector() { 656 | // Fills key_select key list 657 | $("#openpgpjs_key_select_list").html(""); 658 | 659 | // Only one key in keyring, nothing to select from 660 | if(getPrivkeyCount() === 1) { 661 | $("#openpgpjs_selected_id").val(0); 662 | } else { 663 | // Selected set as $("#openpgpjs_selected_id").val(), then get that value from set_passphrase 664 | for (var i = 0; i < getPrivkeyCount(); i++) { 665 | for (var j = 0; j < getKeyUserids(i, true).length; j++) { 666 | fingerprint = getFingerprint(i, true, false); 667 | person = escapeHtml(getPerson(i, j, true)); 668 | $("#openpgpjs_key_select_list").append("
" + fingerprint + " " + person + "
"); 669 | } 670 | } 671 | 672 | $("#openpgpjs_key_select_list").append("
" + rcmail.gettext("selected", "rc_openpgpjs") + ": " + rcmail.gettext("none", "rc_openpgpjs") + "
"); 673 | } 674 | 675 | return true; 676 | } 677 | 678 | /** 679 | * Updates key manager public keys table, private keys table 680 | * and identy selector. 681 | */ 682 | function updateKeyManager() { 683 | // fill key manager public key table 684 | $("#openpgpjs_pubkeys tbody").empty(); 685 | for (var i = 0; i < getPubkeyCount(); i++) { 686 | var key_id = getKeyID(i); 687 | var fingerprint = getFingerprint(i); 688 | var person = escapeHtml(getPerson(i, 0)); 689 | var length_alg = getAlgorithmString(i); 690 | var status = (verifyBasicSignatures(i) ? rcmail.gettext("valid", "rc_openpgpjs") : rcmail.gettext("invalid", "rc_openpgpjs")); 691 | var del = "" + rcmail.gettext('delete', 'rc_openpgpjs') + ""; 692 | var exp = "Export "; 693 | 694 | var result = "" + 695 | "" + key_id + "" + 696 | "" + fingerprint + "" + 697 | "" + person + "" + 698 | "" + length_alg + "" + 699 | "" + status + "" + 700 | "" + exp + del + "" + 701 | ""; 702 | $("#openpgpjs_pubkeys tbody").append(result); 703 | } 704 | 705 | // fill key manager private key table 706 | $("#openpgpjs_privkeys tbody").empty(); 707 | for (var i = 0; i < getPrivkeyCount(); i++) { 708 | for (var j = 0; j < getKeyUserids(i, true).length; j++) { 709 | var key_id = getKeyID(i, true); 710 | var fingerprint = getFingerprint(i, true); 711 | var person = escapeHtml(getPerson(i, j, true)); 712 | var length_alg = getAlgorithmString(i, true); 713 | var del = "" + rcmail.gettext('delete', 'rc_openpgpjs') + ""; 714 | var exp = "Export "; 715 | 716 | var result = "" + 717 | "" + key_id + "" + 718 | "" + fingerprint + "" + 719 | "" + person + "" + 720 | "" + length_alg + "" + 721 | "" + exp + del + "" + 722 | ""; 723 | 724 | $("#openpgpjs_privkeys tbody").append(result); 725 | } 726 | } 727 | 728 | // fill key manager generation identity selector 729 | $("#gen_ident").html(""); 730 | identities = JSON.parse($("#openpgpjs_identities").html()); 731 | for (var i = 0; i < identities.length; i++) { 732 | $("#gen_ident").append(""); 733 | } 734 | } 735 | 736 | /** 737 | * Converts an algorithm id (1/2/3/16/17) to the 738 | * corresponding algorithm type 739 | * 740 | * @param id {Integer} Algorithm id 741 | * @return {String} Algorithm type 742 | */ 743 | function typeToStr(id) { 744 | var r = "" 745 | 746 | switch(id) { 747 | case 1: 748 | r = "RSA(S/E)"; 749 | break; 750 | case 2: 751 | r = "RSA(E)"; 752 | break; 753 | case 3: 754 | r = "RSA(S)"; 755 | break; 756 | case 16: 757 | r = "Elg"; 758 | break; 759 | case 17: 760 | r = "DSA"; 761 | break; 762 | default: 763 | r = "UNKNOWN"; 764 | break; 765 | } 766 | 767 | return(r); 768 | } 769 | 770 | /** 771 | * Escape some unsafe characters into their html entities. 772 | * 773 | * @param unsafe {String} Unsafe string to escape 774 | */ 775 | function escapeHtml(unsafe) { 776 | return unsafe.replace(/&/g, "&") 777 | .replace(//g, ">") 779 | .replace(/"/g, """) 780 | .replace(/'/g, "'"); 781 | } 782 | 783 | function showMessages(msg) { console.log(msg); } 784 | 785 | /** 786 | * Display a custom message above the email body, analogous to 787 | * Roundcubes privacy warning message. 788 | * 789 | * @param msg {String} Message to display 790 | * @param type {String} One of 'confirmation', 'notice', 'error' 791 | */ 792 | function displayUserMessage(msg, type) { 793 | // Insert a div into the message-objects
provided by Roundcube 794 | $('
').text(msg).addClass(type).addClass('messagepadding').appendTo($('#message-objects')); 795 | } 796 | } 797 | -------------------------------------------------------------------------------- /localization/de_DE.inc: -------------------------------------------------------------------------------- 1 | 2 | 6 | rc_openpgpjs 7 | https://github.com/qnrq/rc_openpgpjs 8 | Security plugin for Roundcube 9 | 10 | rc_openpgpjs is an extension adding OpenPGP functionality to the Roundcube webmail project. 11 | 12 | 13 | Niklas Femerstrand 14 | qnrq 15 | nik@qnrq.se 16 | yes 17 | 18 | 2013-01-07 19 | 20 | 0.1 21 | 0.1 22 | 23 | 24 | unstable 25 | unstable 26 | 27 | GNU GPLv2 28 | - 29 | 30 | 31 | 32 | 5.2.2 33 | 34 | 35 | 1.7.0 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /pygpghttpd/DOCS/API.txt: -------------------------------------------------------------------------------- 1 | Generating keys 2 | --------------- 3 | 4 | Generates key pair 5 | 6 | {String} cmd keygen 7 | {String} type {RSA,DSA} 8 | {Integer} length Key length in bits 9 | {String} name Name of key owner 10 | {String} email Email address for key pair 11 | {String} passphrase Passphrase for private key 12 | 13 | Listing keys 14 | ------------ 15 | 16 | Lists keys in keyring 17 | 18 | {String} cmd keylist 19 | {Boolean} private Default false 20 | 21 | Deleting keys 22 | ------------- 23 | 24 | To delete keys, their key identifiers must be specified. If a public/private 25 | keypair has been created, a private key needs to be deleted before the public 26 | key can be deleted. 27 | 28 | {String} cmd keydel 29 | {Boolean} private Default false 30 | {String} fingerprint Fingerprint for key to delete 31 | 32 | Exporting keys 33 | -------------- 34 | 35 | For security reasons only public keys can be exported through pygpghttpd. 36 | 37 | {String} cmd keyexport 38 | {String} id ID or fingerprint for key to export 39 | 40 | Importing keys 41 | -------------- 42 | 43 | {String} cmd keyimport 44 | {String} key ASCII armored key 45 | 46 | Encryption 47 | ---------- 48 | 49 | {String} cmd encrypt 50 | {String} data Data to encrypt 51 | {String} recipients JSON encoded array containing recipient fingerprints 52 | {String} sign The fingerprint of a key which is used to sign the encrypted data. When not specified, the data is not signed 53 | {String} passphrase Passphrase for key used to sign the encrypted data. Required if data is being signed 54 | 55 | Decryption 56 | ---------- 57 | 58 | {String} cmd decrypt 59 | {String} data Data to decrypt 60 | {String} passphrase Passphrase for key used to decrypt the data 61 | 62 | Signing 63 | ------- 64 | 65 | {String} cmd sign 66 | {String} data Data to sign 67 | {String} keyid ID of private key to use 68 | {String} passphrase Passphrase of the private key 69 | 70 | Verification 71 | ------------ 72 | 73 | {String} cmd verify 74 | {String} data Data to verify 75 | -------------------------------------------------------------------------------- /pygpghttpd/LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | -------------------------------------------------------------------------------- /pygpghttpd/README.md: -------------------------------------------------------------------------------- 1 | pygpghttpd 2 | ========== 3 | pygpghttpd exposes an API enabling GnuPG's cryptographic functionality to be 4 | used in web browsers and other software which allows HTTP requests. pygpghttpd 5 | runs on the client's localhost and allows calling GnuPG binaries from the 6 | user's browser securely without exposing cryptograhically sensitive data to 7 | hostile environments. pygpghttpd bridges the required elements of GnuPG to HTTP 8 | allowing its cryptographic functionality to be called without the need to trust 9 | JavaScript based PGP/GPG ports. As pygpghttpd calls local GnuPG binaries it is 10 | also using local keyrings and relying on it entirely for strength. In short 11 | pygpghttpd is just a dummy task router between browser and GnuPG binary. 12 | 13 | How it works 14 | ------------ 15 | pygpghttpd acts as a HTTPS server listening on port 11337 for POST requests 16 | containing operation commands and parameters to execute. When a request is 17 | received it checks the "Origin", or if missing the "Referer", HTTP header to 18 | find out which domain served the content that is contacting it. It then 19 | detects if the domain is added to the "accepted\_domains.txt" file by the user 20 | to ensure that it is only operational for pre accepted domains. If the 21 | referring domain is accepted it treats the request and serves the result 22 | from the local GnuPG binary to the client. In the response a [Cross-origin 23 | resource sharing HTTP][cors] header is sent to inform the user's browser that 24 | the request should be permitted. If the referring domain is missing from 25 | accepted_domains.txt the user's browser forbids the request in accordance with 26 | the [same origin security policy][same origin]. 27 | 28 | The HTTPS certificate used by pygpghttpd is self signed and is not used with 29 | the intention to enhance security since all traffic is isolated to the 30 | local network interface. It uses HTTPS to ensure that both HTTPS and HTTP 31 | delivered content can interact with it. 32 | 33 | pygpghttpd exposes metadata for both private and public keys but only 34 | allows public keys to be exported from the local keyring. The metadata for private keys is 35 | enough for performing cryptographic actions. Complete keypairs can be 36 | generated and imported into the local keyring. 37 | 38 | For example, generating a keypair with cURL: 39 | 40 | *curl -k --data "cmd=keygen&type=RSA&length=2048&name=Alice&email=alice@foo.com&passphrase=foobar" -H "Origin: https://accepted.domain.com" https://localhost:11337/* 41 | 42 | Please see the [API documentation][api] for full details. 43 | 44 | Installation 45 | ------------ 46 | Compile and install all dependencies. If you are running Windows then check the 47 | [python-gnupg][python-gnupg] documentation notes for Windows usage. Windows 48 | binaries will be distributed later. 49 | 50 | Before using pygphttpd with content delivered from other than localhost you 51 | need to open the accepted\_domains.txt file and add them separated by newlines. 52 | 53 | After you have started pygphttpd ensure that the browser that you are using 54 | supports TLSv1.1 or TLSv1.2, otherwise you'll get SSL failures. Then visit 55 | https://localhost:11337/ and add a permanent exception for the used certificate 56 | so your browser can communicate with it properly. If you for some reason wish 57 | to replace the included certificate file you can do that by: 58 | 59 | *openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout cert.pem* 60 | 61 | Dependencies 62 | ------------ 63 | 64 | [Python](http://www.python.org/) 65 | 66 | [GnuPG](http://www.gnupg.org/) 67 | 68 | [python-gnupg](https://code.google.com/p/python-gnupg/) 69 | 70 | [cors]: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing 71 | [same origin]: https://en.wikipedia.org/wiki/Same_origin_policy 72 | [python-gnupg]: https://pythonhosted.org/python-gnupg/ 73 | [api]: https://raw.github.com/qnrq/pygphttpd/master/DOCS/API.txt 74 | -------------------------------------------------------------------------------- /pygpghttpd/accepted_domains.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # THIS FILE CONTAINS THE LIST OF DOMAINS THAT YOU PERMIT READ YOUR LOCAL GPG # 3 | # DATA. THIS FILE IS THE ENTIRE SECURITY MECHANISM GUARDING YOUR GPG SECRETS # 4 | # SO TREAT IT THAT WAY. # 5 | ################################################################################ 6 | # 7 | # List each domain that you permit access your GPG data on separate lines. 8 | # localhost is included as an example. 9 | localhost 10 | -------------------------------------------------------------------------------- /pygpghttpd/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALYfv8w5diCBgtn6 3 | 5h4V0uQt9YdkSKMHTOBNktqArfR1uEsH752VFBDU1XeXU5VAu7mpWoaLF+vPNKII 4 | ZQ1vb+hPrhOULzfsv9rWgMlrOb5trwJ7OMDb4QViA5+c1n3VqrWHeeJhXvASfX4L 5 | N45btQmnq7SL1A6vKVBF9HhA2Y23AgMBAAECgYB+D73fq4pzd5HONhfgjTSbkqBX 6 | 5fdNOTliLO/QZK+D/ZPiA409IzpvaBKWI93L+rG2El5BsLePFq6U1YC06wSmuoiq 7 | ZnZ/W4aDU/qvxoleAWY/mEStpWnPca/wx0zCbpYDECSEFY6UBhKEhO2/picXxk8c 8 | 6AVWo5jPRl5NzRFTWQJBAOE9ELUzRb0DFAsn+GRXCg+V6rI9TX2qTKfo3u9p2yf+ 9 | V1zsCTIl4GZKusZHDMzaykSyr1axSxxUD20MDYrzfvsCQQDO/0lWyaoniF4P3Or0 10 | 95IXUUc+LSzB+X9dcDI7XP5aWibQsnj4ZkXIgJqg/4lokzWWY9v814L3+2/iPR3+ 11 | Fn91AkEA0eA/0Eg9ZFkY9ShCeOTtuAYekgUzTrRAB3mjOf0uO/7wUuKR0wueJFLf 12 | 5N/RmpUIQqkpXqOHdJcTZK4FdINdvwJAbV2QIByYnB5+pB7yvM75DvzQiVdQ6IOr 13 | +XBH+fleIdqz21wQch2HDTJ1gE7DCM+OZpEIMASlm+Pq7zufVxoH6QJBAKDfkNNR 14 | vgIbD8p+DiNibOSZLAGlEypk4kgEM8atDdd+RbN1PoVtIR+F86BoVAhMoDrG9gAs 15 | FodAlKLwcOOkqWU= 16 | -----END PRIVATE KEY----- 17 | -----BEGIN CERTIFICATE----- 18 | MIICvDCCAiWgAwIBAgIJANsMuoQ2yfxkMA0GCSqGSIb3DQEBBQUAMHcxCzAJBgNV 19 | BAYTAktIMRMwEQYDVQQIDApTb21lLVN0YXRlMRMwEQYDVQQHDApQaG5vbSBQZW5o 20 | MQ0wCwYDVQQKDARxbnJxMRMwEQYDVQQDDApweWdwZ2h0dHBkMRowGAYJKoZIhvcN 21 | AQkBFgtuaWtAcW5ycS5zZTAeFw0xMzA3MDcxNjMwMTlaFw0xNDA3MDcxNjMwMTla 22 | MHcxCzAJBgNVBAYTAktIMRMwEQYDVQQIDApTb21lLVN0YXRlMRMwEQYDVQQHDApQ 23 | aG5vbSBQZW5oMQ0wCwYDVQQKDARxbnJxMRMwEQYDVQQDDApweWdwZ2h0dHBkMRow 24 | GAYJKoZIhvcNAQkBFgtuaWtAcW5ycS5zZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw 25 | gYkCgYEAth+/zDl2IIGC2frmHhXS5C31h2RIowdM4E2S2oCt9HW4SwfvnZUUENTV 26 | d5dTlUC7ualahosX6880oghlDW9v6E+uE5QvN+y/2taAyWs5vm2vAns4wNvhBWID 27 | n5zWfdWqtYd54mFe8BJ9fgs3jlu1CaertIvUDq8pUEX0eEDZjbcCAwEAAaNQME4w 28 | HQYDVR0OBBYEFOqdiGAeYdOJAb0lGgPIuRvW9MX3MB8GA1UdIwQYMBaAFOqdiGAe 29 | YdOJAb0lGgPIuRvW9MX3MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA 30 | fdqWHAeOnWBn1+IKrW4buhYHE0jb5g4f9x15GHFBSuI8CIdZE4I4Fl6Rk1JB6Qdi 31 | Pe8JOrU0fU1qKzwe2NP3zM9zhVO74YSthDYf0dt/xY8Eh2pYO4Y/o9Qr5uKpTJK7 32 | V+93xvGIJgzCuXnM5jHaXaW3979EE7KY++HxIFtc/bw= 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /pygpghttpd/example/example.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | pygpghttpd test suite 8 | 9 | 10 |

pygpghttpd test suite

11 |
12 | 23 | 24 |

Pubkeys

25 | 26 | 27 |
KeyidFingerprintAlgoLengthDelete
28 |

Privkeys

29 | 30 | 31 |
KeyidFingerprintAlgoLengthDelete
32 | 33 |

Encrypt

34 | 35 | 36 |

Decrypt

37 | 38 | 39 |
40 | 41 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /pygpghttpd/pygpghttpd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # This HTTPD acts bridge to make OpenPGP functionality accessible for # 5 | # JavaScript through locally installed GnuPG binaries and keyrings. # 6 | # # 7 | # Copyright (C) Niklas Femerstrand # 8 | # # 9 | # This program is free software; you can redistribute it and/or modify it # 10 | # under the terms of the GNU General Public License version 2 as published by # 11 | # the Free Software Foundation. # 12 | # # 13 | # This program is distributed in the hope that it will be useful, but WITHOUT # 14 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # 16 | # more details. # 17 | # # 18 | # You should have received a copy of the GNU General Public License along # 19 | # with this program; if not, write to the Free Software Foundation, Inc., # 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 21 | ############################################################################### 22 | # iM4G1N3 A FR33 W0RLD Wh3R3 0n3 W0uLDN'T N33D K0P1R19H7 2 S4Y N0 2 K0PiFi9H7 # 23 | ############################################################################### 24 | 25 | import re, socket, ssl, sys, gnupg, json 26 | import _thread as thread 27 | from os.path import expanduser 28 | import urllib.parse 29 | 30 | home = expanduser("~") 31 | home += "/.gnupg/" 32 | gpg = gnupg.GPG(gnupghome = home) 33 | gpg.encoding = "utf-8" 34 | 35 | def deal_with_client(connstream): 36 | data = connstream.recv(8192).decode("utf-8") 37 | m = re.search("^(GET|POST)", data) 38 | if not m: 39 | data += connstream.recv(8192).decode("utf-8") 40 | 41 | while data: 42 | dd = data.split("\n") 43 | cmdstr = "" 44 | origin = "" 45 | referer = "" 46 | 47 | for header in dd: 48 | print(header) 49 | m = re.search("^Origin: (.*)$", header) 50 | if m: 51 | origin = m.groups()[0].rstrip() 52 | 53 | # Fetch domain from referer header, some browsers always provide origin. 54 | m = re.search("^Referer: (.*)/login", header) 55 | if m: 56 | referer = m.groups()[0].rstrip() 57 | 58 | if "=" in header: 59 | cmdstr = header 60 | 61 | # Fall back on referer 62 | if not origin: 63 | origin = referer 64 | 65 | if not do_something(connstream, data, origin, cmdstr): 66 | break 67 | 68 | def do_something(connstream, data, origin, cmdstr): 69 | allow_request = "" 70 | response = "" 71 | 72 | cors = ["null"] 73 | with open("accepted_domains.txt", 'r') as f: 74 | lines = f.readlines() 75 | for line in lines: 76 | if not line.startswith("#"): 77 | cors.append(line.replace("\r", "").replace("\n", "")) 78 | 79 | o = origin.replace("http://", "").replace("https://", "") 80 | if o not in cors: 81 | response = "Illegal origin" 82 | else: 83 | allow_request = 1 84 | response = do_gpg(cmdstr) 85 | 86 | content_length = str(len(response)) 87 | 88 | if allow_request: 89 | connstream.write("HTTP/1.1 200 OK\r\n".encode()) 90 | else: 91 | connstream.write("HTTP/1.1 403 Forbidden\r\n".encode()) 92 | connstream.write(("Content-Length: " + content_length + "\r\n").encode()) 93 | connstream.write("Content-Type: text/html\r\n".encode()) 94 | connstream.write("Server: pygpghttpd\r\n".encode()) 95 | 96 | if allow_request: 97 | connstream.write(("Access-Control-Allow-Origin: " + origin + "\r\n").encode()) 98 | connstream.write("\r\n".encode()) 99 | try: 100 | connstream.write(response.encode()) 101 | except: 102 | connstream.write(response) # already utf8 103 | 104 | def do_gpg(cmdstr): 105 | c = {} 106 | cmds_ok = ["keygen", "keylist", "keydel", "keyexport", "keyimport", "encrypt", "decrypt", "sign", "verify"] 107 | 108 | if "&" in cmdstr: # Multiple params 109 | cmds = cmdstr.split("&") 110 | for cmd in cmds: 111 | cc = cmd.split("=") 112 | if cc[0] and cc[1]: 113 | c[cc[0]] = urllib.parse.unquote(cc[1]) 114 | else: # Single param 115 | cc = cmdstr.split("=") 116 | if cc[0] and cc[1]: 117 | c[cc[0]] = urllib.parse.unquote(cc[1]) 118 | 119 | if "cmd" not in c: 120 | return("Missing cmdstr for GPG op") 121 | 122 | for cmd_ok in cmds_ok: 123 | if cmd_ok == c["cmd"]: 124 | return(globals()[c["cmd"]](c)) 125 | 126 | return("Unsupported cmdstr") 127 | 128 | def keylist(cmd): 129 | if "private" not in cmd: 130 | cmd["private"] = False 131 | else: 132 | if cmd["private"] == "true" or cmd["private"] == "1": 133 | cmd["private"] = True 134 | else: 135 | cmd["private"] = False 136 | 137 | keys = gpg.list_keys(cmd["private"]) 138 | return(json.dumps(keys)) 139 | 140 | def keygen(cmd): 141 | required = ["type", "length", "name", "email", "passphrase"] 142 | key_types = ["RSA", "DSA"] 143 | key_lengths = ["2048", "4096"] 144 | 145 | for req in required: 146 | if req not in cmd: 147 | return("Insufficient parameters: %s" % (req)) 148 | 149 | if cmd["type"] not in key_types: 150 | return("Incorrect: type") 151 | 152 | if cmd["length"] not in key_lengths: 153 | return("Incorrect: length") 154 | 155 | input_data = gpg.gen_key_input(key_type = cmd["type"], key_length = cmd["length"], name_real = cmd["name"], name_email = cmd["email"], passphrase = cmd["passphrase"], name_comment = "pygpghttpd") 156 | key = gpg.gen_key(input_data) 157 | 158 | if key: 159 | return("1") 160 | return("0") 161 | 162 | def keydel(cmd): 163 | if "private" not in cmd: 164 | cmd["private"] = False 165 | else: 166 | if cmd["private"] == "true" or cmd["private"] == "1": 167 | cmd["private"] = True 168 | else: 169 | cmd["private"] = False 170 | 171 | if "fingerprint" not in cmd: 172 | return("Insufficient parameters: fingerprint") 173 | 174 | return(str(gpg.delete_keys(cmd["fingerprint"], cmd["private"]))) 175 | 176 | # Allow only pubkey export for security 177 | def keyexport(cmd): 178 | if "id" not in cmd: 179 | return("Insufficient parameters: id") 180 | 181 | return(gpg.export_keys(cmd["id"])) 182 | 183 | def keyimport(cmd): 184 | if "key" not in cmd: 185 | return("Insufficient parameters: key") 186 | 187 | return(gpg.import_keys(cmd["key"])) 188 | 189 | def encrypt(cmd): 190 | required = ["data", "recipients"] 191 | 192 | for req in required: 193 | if req not in cmd: 194 | return("Insufficient parameters: %s" % (req)) 195 | 196 | try: 197 | cmd["sign"] 198 | except: 199 | cmd["sign"] = None 200 | cmd["passphrase"] = None 201 | pass 202 | else: 203 | if "passphrase" not in cmd: 204 | return("Insufficient parameters: passphrase (needed since sign is set") 205 | 206 | encrypted = gpg.encrypt(cmd["data"], recipients = cmd["recipients"], sign = cmd["sign"], passphrase = cmd["passphrase"]) 207 | print(encrypted.stderr) 208 | return(str(encrypted)) 209 | 210 | def decrypt(cmd): 211 | required = ["data", "passphrase"] 212 | 213 | for req in required: 214 | if req not in cmd: 215 | return("Insufficient parameters: %s" % (req)) 216 | 217 | # TODO s/+/ / on these specific lines + comment 218 | cmd["data"] = cmd["data"].replace("BEGIN+PGP+MESSAGE", "BEGIN PGP MESSAGE") 219 | cmd["data"] = cmd["data"].replace("END+PGP+MESSAGE", "END PGP MESSAGE") 220 | cmd["data"] = cmd["data"].replace("Version:+GnuPG+v2.0.20+(GNU/Linux)", "Version: GnuPG v2.0.20 (GNU/Linux)") 221 | 222 | decrypted = gpg.decrypt(message = cmd["data"], passphrase = cmd["passphrase"]) 223 | print(decrypted.stderr) 224 | return(decrypted.data) 225 | 226 | def sign(cmd): 227 | required = ["data", "keyid", "passphrase"] 228 | 229 | for req in required: 230 | if req not in cmd: 231 | return("Insufficient parameters: %s" % (req)) 232 | 233 | return(gpg.sign(cmd["data"], keyid = cmd["keyid"], passphrase = cmd["passphrase"])) 234 | 235 | def verify(cmd): 236 | if "data" not in cmd: 237 | return("Insufficient parameters: data") 238 | 239 | return(gpg.verify(cmd["data"])) 240 | 241 | def threadHandler(client, addr): 242 | connstream = "" 243 | 244 | try: 245 | connstream = ssl.wrap_socket(client, 246 | server_side = True, 247 | certfile = "./cert.pem", 248 | keyfile = "./cert.pem", 249 | ssl_version = ssl.PROTOCOL_SSLv23) 250 | except Exception as exception: 251 | print(exception) 252 | 253 | if not connstream: 254 | return 255 | try: 256 | deal_with_client(connstream) 257 | finally: 258 | connstream.shutdown(socket.SHUT_RDWR) 259 | 260 | try: 261 | sock = socket.socket(socket.AF_INET) 262 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 263 | sock.bind(("127.0.0.1", 11337)) 264 | sock.listen(0) 265 | except socket.error: 266 | print("Failed to create socket") 267 | sys.exit() 268 | 269 | while True: 270 | client, addr = sock.accept() 271 | thread.start_new_thread(threadHandler, (client, addr)) 272 | -------------------------------------------------------------------------------- /rc_openpgpjs.php: -------------------------------------------------------------------------------- 1 | | 7 | | | 8 | | This program is free software; you can redistribute it and/or modify | 9 | | it under the terms of the GNU General Public License version 2 | 10 | | as published by the Free Software Foundation. | 11 | | | 12 | | This program is distributed in the hope that it will be useful, | 13 | | but WITHOUT ANY WARRANTY; without even the implied warranty of | 14 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 15 | | GNU General Public License for more details. | 16 | | | 17 | | You should have received a copy of the GNU General Public License along | 18 | | with this program; if not, write to the Free Software Foundation, Inc., | 19 | | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | 20 | | | 21 | +-------------------------------------------------------------------------+ 22 | */ 23 | 24 | class rc_openpgpjs extends rcube_plugin { 25 | public $task = 'mail|settings'; 26 | public $rc; 27 | 28 | /** 29 | * Plugin initialization. 30 | */ 31 | function init() { 32 | $this->version_detect(); 33 | 34 | $this->rc = rcube::get_instance(); 35 | $this->rm = rcmail::get_instance(); 36 | 37 | $this->add_hook('user_create', array($this, 'user_create')); 38 | $this->register_action('plugin.pks_search', array($this, 'hkp_search')); 39 | $this->register_action('plugin.hkp_add', array($this, 'hkp_add')); 40 | $this->register_action('plugin.pubkey_save', array($this, 'pubkey_save')); 41 | 42 | if ($this->rc->task == 'mail') { 43 | $this->add_hook('render_page', array($this, 'render_page')); 44 | 45 | // make localization available on the client 46 | $this->add_texts('localization/', true); 47 | 48 | // load js 49 | $this->include_script('js/openpgp.min.js'); 50 | $this->include_script('js/rc_openpgpjs.crypto.js'); 51 | $this->include_script('js/rc_openpgpjs.js'); 52 | 53 | if(isset($_SESSION["rc_openpgpjs_outdated"])) { 54 | $this->include_script('js/outdated.js'); 55 | } 56 | 57 | // load css 58 | $this->include_stylesheet($this->local_skin_path() . '/rc_openpgpjs.css'); 59 | 60 | // add public key attachment related hooks 61 | $this->add_hook('message_compose', array($this, 'message_compose')); 62 | $this->add_hook('message_sent', array($this, 'unlink_pubkey')); 63 | 64 | if ($this->api->output->type == 'html') { 65 | // add key manager item to message menu 66 | if ( $this->is_enabled()) { 67 | $opts = array("command" => "open-key-manager", 68 | "label" => "rc_openpgpjs.key_manager", 69 | "type" => "link", 70 | "classact" => "icon active", 71 | "class" => "icon", 72 | "innerclass" => "icon key_manager"); 73 | 74 | $this->api->add_content(html::tag('li', null, $this->api->output->button($opts)), "messagemenu"); 75 | } 76 | 77 | if ($this->rc->action == 'compose') { 78 | 79 | if ( $this->is_enabled()) 80 | { 81 | // Add hidden field to determine if warning should be enabled 82 | $openpgp_warn_opts = array('id' => 'openpgpjs_warn', 83 | 'type' => 'hidden', 84 | 'value' => $this->rc->config->get('warn_on_unencrypted', false)?'1':'0' 85 | ); 86 | $opengpg_warn = new html_inputfield($openpgp_warn_opts); 87 | $this->api->add_content( html::span('composeoption', html::label(null, $opengpg_warn->show())), "composeoptions"); 88 | 89 | // add key manager button to compose toolbar 90 | $opts = array("command" => "open-key-manager", 91 | "label" => "rc_openpgpjs.key_manager", 92 | "type" => "link", 93 | "classact" => "button active key_manager", 94 | "class" => "button key_manager"); 95 | $this->api->add_content($this->api->output->button($opts), "toolbar"); 96 | 97 | // add encrypt and sign checkboxes to composeoptions 98 | $encrypt_opts = array('id' => 'openpgpjs_encrypt', 99 | 'type' => 'checkbox'); 100 | if($this->rc->config->get('encrypt', false)) { 101 | $encrypt_opts['checked'] = 'checked'; 102 | } 103 | $encrypt = new html_inputfield($encrypt_opts); 104 | $this->api->add_content( 105 | html::span('composeoption', html::label(null, $encrypt->show() . $this->gettext('encrypt'))), 106 | "composeoptions" 107 | ); 108 | $sign_opts = array('id' => 'openpgpjs_sign', 109 | 'type' => 'checkbox'); 110 | if($this->rc->config->get('sign', false)) { 111 | $sign_opts['checked'] = 'checked'; 112 | } 113 | $sign = new html_inputfield($sign_opts); 114 | $this->api->add_content( 115 | html::span('composeoption', html::label(null, $sign->show() . $this->gettext('sign'))), 116 | "composeoptions" 117 | ); 118 | } 119 | } 120 | } 121 | } elseif ($this->rc->task == 'settings') { 122 | // load localization 123 | $this->add_texts('localization/', false); 124 | 125 | // load style sheet 126 | $this->include_stylesheet($this->local_skin_path() . '/rc_openpgpjs.css'); 127 | 128 | // add hooks for OpenPGP settings 129 | $this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list')); 130 | $this->add_hook('preferences_list', array($this, 'preferences_list')); 131 | $this->add_hook('preferences_save', array($this, 'preferences_save')); 132 | } 133 | } 134 | 135 | // Match remote version string with local version string to detect outdated plugin installs 136 | private function version_detect() { 137 | /** 138 | * TODO: Setup listening httpd somewhere to serve latest file. Requires some infrastructure, like a website for the proj. This is TEMP. 139 | */ 140 | if(!isset($_SESSION["rc_openpgpjs_ver"]) || $_SESSION["rc_openpgpjs_ver"] < date("Ymd")) { 141 | $local_src = file_get_contents(__DIR__."/js/rc_openpgpjs.js"); 142 | if(ini_get("allow_url_fopen") === "1") { 143 | $remote_src = file_get_contents("https://raw.github.com/qnrq/rc_openpgpjs/master/js/rc_openpgpjs.js"); 144 | } elseif(ini_get("allow_url_fopen") != "1") { 145 | if(!function_exists("curl_init")) { 146 | // TODO: Add failure notif msg 147 | return false; 148 | } 149 | 150 | $ch = curl_init(); 151 | curl_setopt($ch, CURLOPT_URL, "https://raw.github.com/qnrq/rc_openpgpjs/master/js/rc_openpgpjs.js"); 152 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 153 | $remote_src = curl_exec($ch); 154 | 155 | if(curl_errno($ch)) { 156 | // TODO: Add failure notif msg 157 | return false; 158 | } 159 | 160 | curl_close($ch); 161 | } 162 | 163 | preg_match("/var VERSTR = \"(.*)\"/", $remote_src, $remoteM); 164 | preg_match("/var VERSTR = \"(.*)\"/", $local_src, $localM); 165 | 166 | if(isset($remoteM[1]) && isset($localM[1])) { 167 | if($remoteM[1] != $localM[1]) { 168 | $_SESSION["rc_openpgpjs_outdated"] = 1; 169 | } 170 | } 171 | 172 | $_SESSION["rc_openpgpjs_ver"] = date("Ymd"); // Checking once a day per session should be fine 173 | } 174 | } 175 | 176 | private function is_enabled() 177 | { 178 | return $this->rc->config->get('openpgp_enabled', 1); 179 | } 180 | 181 | /** 182 | * Add key manager and key selector to html output 183 | * 184 | * @param array Original parameters 185 | * @return array Modified parameters 186 | */ 187 | function render_page($params) { 188 | $template_path = $this->home . '/'. $this->local_skin_path(); 189 | $this->rc->output->add_footer($this->rc->output->just_parse( 190 | file_get_contents($template_path . '/templates/key_manager.html') . 191 | file_get_contents($template_path . '/templates/key_search.html') . 192 | file_get_contents($template_path . '/templates/key_select.html'))); 193 | $this->rc->output->add_footer(html::div(array('style' => "visibility: hidden;", 194 | 'id' => "openpgpjs_identities"), 195 | json_encode($this->rm->user->list_identities()))); 196 | 197 | return $params; 198 | } 199 | 200 | /** 201 | * Create default identity, required as pubkey metadata 202 | */ 203 | function user_create($params) { 204 | $params['user_name'] = preg_replace("/@.*$/", "", $params['user']); 205 | $params['user_email'] = $params['user']; 206 | return $params; 207 | } 208 | 209 | /** 210 | * This Public Key Server proxy is written to circumvent Access-Control-Allow-Origin 211 | * limitations. It also provides a layer of security as HKP normally doesn't 212 | * support HTTPS; essentially preventing MITM if the Roundcube installation 213 | * is configured to use HTTPS. 214 | * 215 | * For more details see the following: 216 | * http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 217 | * http://sks-keyservers.net/ 218 | * 219 | * Please use http://pool.sks-keyservers.net as the source for this proxy 220 | */ 221 | function hkp_search() { 222 | if(!isset($_POST['op']) || !isset($_POST['search'])) { 223 | return $this->rc->output->command( 224 | 'plugin.pks_search', 225 | array('message' => "ERR: Missing param", 226 | 'op' => htmlspecialchars($_POST['op']))); 227 | $op = ""; 228 | $search = ""; 229 | } else { 230 | $op = $_POST["op"]; 231 | $search = $_POST["search"]; 232 | } 233 | 234 | if($op != "get" && 235 | $op != "index" && 236 | $op != "vindex") 237 | return $this->rc->output->command( 238 | 'plugin.pks_search', 239 | array('message' => "ERR: Invalid operation", 240 | 'op' => htmlspecialchars($op))); 241 | 242 | if($op == "index") { 243 | $ch = curl_init(); 244 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 245 | curl_setopt($ch, CURLOPT_URL, "http://pool.sks-keyservers.net:11371/pks/lookup?op=index&search={$search}"); 246 | $result = curl_exec($ch); 247 | $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); 248 | curl_close($ch); 249 | 250 | if($status == 200) { 251 | // TODO Fix search regex to match 32/64-bit str 252 | preg_match_all("/\/pks\/lookup\?op=vindex&search=(.*)\">(.*)<\/a>/", $result, $m); 253 | 254 | if(count($m[0]) > 0) { 255 | $found = array(); 256 | for($i = 0; $i < count($m[0]); $i++) 257 | $found[] = array($m[1][$i], $m[2][$i]); 258 | return $this->rc->output->command( 259 | 'plugin.pks_search', 260 | array('message' => json_encode($found), 261 | 'op' => htmlspecialchars($op))); 262 | } 263 | } else { 264 | preg_match("/Error handling request: (.*)<\/body>/", $result, $m); 265 | return $this->rc->output->command( 266 | 'plugin.pks_search', 267 | array('message' => "ERR: " . htmlspecialchars($m[1]), 268 | 'op' => htmlspecialchars($op))); 269 | } 270 | } elseif($op == "get") { 271 | if(preg_match("/^0x[0-9A-F]{8}$/i", $search)) { 272 | define("32_BIT_KEY", true); 273 | define("64_BIT_KEY", false); 274 | } elseif(preg_match("/^0x[0-9A-F]{16}$/i", $search)) { 275 | define("32_BIT_KEY", false); 276 | define("64_BIT_KEY", true); 277 | } else { 278 | return $this->rc->output->command( 279 | 'plugin.pks_search', 280 | array('message' => "ERR: Incorrect search format for this operation", 281 | 'op' => htmlspecialchars($op))); 282 | } 283 | 284 | $ch = curl_init(); 285 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 286 | curl_setopt($ch, CURLOPT_URL, "http://pool.sks-keyservers.net:11371/pks/lookup?op=get&search={$search}"); 287 | $result = curl_exec($ch); 288 | $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); 289 | curl_close($ch); 290 | 291 | if($status == 200) { 292 | preg_match_all("/-----BEGIN PGP PUBLIC KEY BLOCK-----(.*)-----END PGP PUBLIC KEY BLOCK-----/s", $result, $m); 293 | return $this->rc->output->command( 294 | 'plugin.pks_search', 295 | array('message' => json_encode($m), 296 | 'op' => htmlspecialchars($op))); 297 | } 298 | } 299 | } 300 | 301 | // TODO: Store pubkeys in rc storage 302 | // Don't sync upstream, it hurts decentralization 303 | function hkp_add() { 304 | header("HTTP/1.1 501 Not Implemented"); 305 | die(); 306 | } 307 | 308 | /** 309 | * Saves the public key to a temporary file so we can send it as attachment 310 | */ 311 | function pubkey_save() { 312 | $rcmail = rcmail::get_instance(); 313 | $temp_dir = unslashify($rcmail->config->get('temp_dir')); 314 | $file = $temp_dir."/".md5($_SESSION['username']).".asc"; 315 | if(file_exists($file)) { 316 | $pubkey = trim(get_input_value('_pubkey', RCUBE_INPUT_POST)); 317 | file_put_contents($file, $pubkey); 318 | } 319 | } 320 | 321 | /** 322 | * Handler for preferences_sections_list hook. 323 | * Add new section to preferences for encryption 324 | * 325 | * @param array Original params 326 | * @return array Modified params 327 | */ 328 | function preferences_sections_list($p) { 329 | $this->add_texts('locallization/', false); 330 | $p['list']['openpgp_prefs'] = array( 331 | 'id' => 'openpgp_prefs', 332 | 'section' => Q($this->gettext('openpgp_title')) 333 | ); 334 | return ($p); 335 | } 336 | 337 | 338 | /** 339 | * Handler for preferences_list hook. 340 | * Adds options blocks into Compose settings sections in Preferences. 341 | * 342 | * @param array Original parameters 343 | * @return array Modified parameters 344 | */ 345 | function preferences_list($p) { 346 | if (!get_input_value('_framed', RCUBE_INPUT_GPC) && $args['section'] == 'openpgp_prefs') { 347 | $p['blocks'][$args['section']]['options'] = array ( 348 | 'title' => '', 349 | 'content' => html::tag('div', array('id' => 'pm_dummy'), '') 350 | ); 351 | return $p; 352 | } 353 | 354 | if ($p['section'] == 'openpgp_prefs') { 355 | $p['blocks']['openpgp']['name'] = $this->gettext('openpgp_options'); 356 | 357 | if (!isset($no_override['openpgp_enabled'])) 358 | { 359 | $field_id = 'rcmfd_openpgp_enabled'; 360 | $enabled = new html_checkbox( 361 | array('name' => '_encEnabled', 362 | 'id' => $field_id, 363 | 'value' => 1, 364 | 'onchange' => "$('#rcmfd_auto_attach_key').prop('disabled', !(this.checked)); " . 365 | "$('#rcmfd_warn_unencrypted').prop('disabled', !(this.checked)); " . 366 | "$('#rcmfd_encrypt').prop('disabled', !(this.checked)); " . 367 | "$('#rcmfd_sign').prop('disabled', !(this.checked))" 368 | )); 369 | $p['blocks']['openpgp']['options']['enabled'] = array( 370 | 'title' => html::label($field_id, Q($this->gettext('openpgp_enabled'))), 371 | 'content' => $enabled->show($this->is_enabled()) 372 | ); 373 | } 374 | 375 | /* Warn on encrypted checkbox */ 376 | $field_id = 'rcmfd_warn_unencrypted'; 377 | $warn = new html_checkbox( 378 | array('name' => '_warn', 379 | 'id' => $field_id, 380 | 'value' => 1, 381 | 'disabled' => !$this->is_enabled() 382 | ) 383 | ); 384 | 385 | $p['blocks']['openpgp']['options']['warn'] = array( 386 | 'title' => html::label($field_id, Q($this->gettext('warn_on_unencrypted'))), 387 | 'content' => $warn->show($this->rc->config->get('warn_on_unencrypted', false)?1:0), 388 | ); 389 | 390 | /* Auto attach checkbox */ 391 | $field_id = 'rcmfd_auto_attach_key'; 392 | $attach = new html_checkbox( 393 | array('name' => '_attachKey', 394 | 'id' => $field_id, 395 | 'value' => 1, 396 | 'disabled' => !$this->is_enabled() 397 | ) 398 | ); 399 | 400 | $p['blocks']['openpgp']['options']['attachKey'] = array( 401 | 'title' => html::label($field_id, Q($this->gettext('always_attachKey'))), 402 | 'content' => $attach->show($this->rc->config->get('attachKey', false)?1:0), 403 | ); 404 | 405 | /* Encrypt default checkbox */ 406 | $field_id = 'rcmfd_encrypt'; 407 | $encrypt = new html_checkbox( 408 | array('name' => '_encrypt', 409 | 'id' => $field_id, 410 | 'value' => 1, 411 | 'disabled' => !$this->is_enabled() 412 | ) 413 | ); 414 | $p['blocks']['openpgp']['options']['encrypt'] = array( 415 | 'title' => html::label($field_id, Q($this->gettext('always_encrypt'))), 416 | 'content' => $encrypt->show($this->rc->config->get('encrypt', false)?1:0), 417 | ); 418 | 419 | /* Sign default checkbox */ 420 | $field_id = 'rcmfd_sign'; 421 | $sign = new html_checkbox( 422 | array('name' => '_sign', 423 | 'id' => $field_id, 424 | 'value' => 1, 425 | 'disabled' => !$this->is_enabled() 426 | ) 427 | ); 428 | $p['blocks']['openpgp']['options']['sign'] = array( 429 | 'title' => html::label($field_id, Q($this->gettext('always_sign'))), 430 | 'content' => $sign->show($this->rc->config->get('sign', false)?1:0), 431 | ); 432 | 433 | } 434 | 435 | return $p; 436 | } 437 | 438 | /** 439 | * Handler for preferences_save hook. 440 | * Executed on Compose settings form submit. 441 | * 442 | * @param array Original parameters 443 | * @return array Modified parameters 444 | */ 445 | function preferences_save($p) { 446 | if ($p['section'] == 'openpgp_prefs') 447 | { 448 | $p['prefs']['encrypt'] = get_input_value('_encrypt', RCUBE_INPUT_POST) ? true : false; 449 | $p['prefs']['sign'] = get_input_value('_sign', RCUBE_INPUT_POST) ? true : false; 450 | $p['prefs']['attachKey'] = get_input_value('_attachKey', RCUBE_INPUT_POST) ? true : false; 451 | $p['prefs']['warn_on_unencrypted'] = get_input_value('_warn', RCUBE_INPUT_POST) ? true : false; 452 | $p['prefs']['openpgp_enabled'] = get_input_value('_encEnabled', RCUBE_INPUT_POST) ? '1': '0'; 453 | } 454 | return $p; 455 | } 456 | 457 | /** 458 | * Handler for message_compose hook 459 | * Creates a dummy publick key attachment 460 | */ 461 | function message_compose($args) { 462 | if ($this->is_enabled() && ($this->rc->config->get('attachKey', false)) && ( $f = $this->create_pubkey_dummy() )) { 463 | $args['attachments'][] = array('path' => $f, 'name' => "pubkey.asc", 'mimetype' => "text/plain"); 464 | } 465 | return $args; 466 | } 467 | 468 | /** 469 | * Handler for message_sent hook 470 | * Deletes the public key from the server 471 | */ 472 | function unlink_pubkey($args) { 473 | $rcmail = rcmail::get_instance(); 474 | $temp_dir = unslashify($rcmail->config->get('temp_dir')); 475 | $file = $temp_dir."/".md5($_SESSION['username']).".asc"; 476 | if(file_exists($file)) { 477 | @unlink($file); 478 | } 479 | } 480 | 481 | /** 482 | * Creates a dummy public key file 483 | */ 484 | function create_pubkey_dummy() { 485 | $rcmail = rcmail::get_instance(); 486 | $temp_dir = unslashify($rcmail->config->get('temp_dir')); 487 | if (!empty($temp_dir)) { 488 | $file = $temp_dir."/".md5($_SESSION['username']).".asc"; 489 | if(file_exists($file)) 490 | @unlink($file); 491 | if (file_put_contents($file, " ")) { 492 | return $file; 493 | } 494 | } 495 | return false; 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /skins/larry/images/key_manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasfemerstrand/rc_openpgpjs/e40a145de964fa2f00a603f6fac9a2c264dcdee8/skins/larry/images/key_manager.png -------------------------------------------------------------------------------- /skins/larry/images/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasfemerstrand/rc_openpgpjs/e40a145de964fa2f00a603f6fac9a2c264dcdee8/skins/larry/images/lock.png -------------------------------------------------------------------------------- /skins/larry/images/openpgp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasfemerstrand/rc_openpgpjs/e40a145de964fa2f00a603f6fac9a2c264dcdee8/skins/larry/images/openpgp.png -------------------------------------------------------------------------------- /skins/larry/openpgp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasfemerstrand/rc_openpgpjs/e40a145de964fa2f00a603f6fac9a2c264dcdee8/skins/larry/openpgp.png -------------------------------------------------------------------------------- /skins/larry/rc_openpgpjs.css: -------------------------------------------------------------------------------- 1 | #sections-table #rcmrowopenpgp_prefs td.section { 2 | background: url("images/openpgp.png") 4px 0px no-repeat; 3 | } 4 | 5 | #sections-table #rcmrowopenpgp_prefs.selected td.section { 6 | background: url("images/openpgp.png") 4px -23px no-repeat; 7 | } 8 | 9 | .hidden { 10 | display: none; 11 | visibility: hidden; 12 | } 13 | ul.toolbarmenu li span.icon.key_manager { 14 | background: url(images/key_manager.png) -2px -1px no-repeat; 15 | } 16 | .toolbar a.button.key_manager { 17 | background: url("images/lock.png") center -2px no-repeat; 18 | } 19 | #openpgpjs_key_manager.popupdialog, 20 | #openpgpjs_key_manager.ui-dialog-content { 21 | padding: 0; 22 | } 23 | #openpgpjs_tabs { 24 | min-height: 400px; 25 | } 26 | #openpgpjs_tabs .ui-tabs { 27 | padding: 0; 28 | } 29 | #openpgpjs_tabs .ui-tabs-panel { 30 | background: transparent; 31 | padding: 0; 32 | } 33 | #openpgpjs_tabs .ui-state-error.ui-corner-all, 34 | #openpgpjs_key_select .ui-state-error.ui-corner-all { 35 | padding: 5px; 36 | width: 400px; 37 | margin-bottom: 5px; 38 | } 39 | #openpgpjs_tabs .ui-icon.ui-icon-alert, 40 | #openpgpjs_key_select .ui-icon.ui-icon-alert { 41 | float: left; 42 | margin-right: 5px; 43 | margin-top: 10px; 44 | } 45 | #openpgpjs_key_select .ui-state-error.ui-corner-all { 46 | width: 275px; 47 | } 48 | #openpgpjs_import textarea { 49 | width: 555px; 50 | height: 175px; 51 | } 52 | .clickme { cursor: pointer !important; } 53 | .clickme:hover { background-color: #fff; } 54 | #generated_private, #generated_public { width: 45%; float: left; text-align: left; font-size: 7pt; } 55 | .odd { background-color: #ccc; } 56 | #openpgpjs_search_results { width: 99%; overflow: scroll; } 57 | #openpgpjs_search_results tr:hover { background-color: #fff; } 58 | 59 | /* Additional styles for the message-objects
in mail view; 60 | Roundcube only includes a "notice" style for this area. 61 | These are used for display of signature status of received messages. */ 62 | #message-objects div.confirmation, 63 | #message-objects div.error { 64 | font-weight: bold; 65 | padding: 6px 30px 6px 25px; 66 | background: url(../../../../skins/larry/images/messages.png) 0 5px no-repeat; 67 | cursor: default; 68 | display: block; 69 | white-space: normal; 70 | } 71 | 72 | #message-objects div.confirmation { 73 | color: #093; 74 | border: 1px solid #093; 75 | background-color: #9f9; 76 | background-position: 5px -25px; 77 | } 78 | 79 | #message-objects div.error { 80 | color: #900; 81 | border: 1px solid #900; 82 | background-color: #f99; 83 | background-position: 5px -55px; 84 | } 85 | 86 | #message-objects div.messagepadding { 87 | padding: 8px 12px 8px 30px; 88 | } 89 | 90 | -------------------------------------------------------------------------------- /skins/larry/templates/key_manager.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 |
  • 5 |
  • 6 |
  • 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |

24 | 28 | 29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 |

51 | 55 |

56 | 57 |
58 |

59 |
60 |
61 |
62 |
63 |
64 |

65 | 69 | 70 | 71 | 72 | 73 | 77 |
78 | 79 | 80 |
81 |
82 |
83 |
84 |
85 | -------------------------------------------------------------------------------- /skins/larry/templates/key_search.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /skins/larry/templates/key_select.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 |

9 |

10 |
11 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /test/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | rc_openpgpjs test suite 5 | 6 | 7 |
8 |

rc_openpgpjs test suite

9 | Click here to run all tests. 10 |
11 |
12 |
13 |

Keygen

14 | Bits: 15 | 20 | Passphrase: 21 | 22 | 23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |

Encrypt

32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 |

Sign

42 | 43 | 44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 | 145 | 146 | 147 | --------------------------------------------------------------------------------