package net.i2p.data; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.util.Locale; import java.util.Map; import java.util.Properties; import com.nettgryppa.security.HashCash; import gnu.getopt.Getopt; import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.client.naming.HostTxtEntry; import net.i2p.crypto.DSAEngine; import net.i2p.crypto.KeyGenerator; import net.i2p.crypto.SigType; import net.i2p.util.OrderedProperties; import net.i2p.util.RandomSource; import net.i2p.util.SecureFileOutputStream; /** * This helper class reads and writes files in the * same "eepPriv.dat" format used by the client code. * The format is: *<pre> * - Destination (387 bytes if no certificate, otherwise longer) * - Public key (256 bytes) * - Signing Public key (128 bytes) * - Cert. type (1 byte) * - Cert. length (2 bytes) * - Certificate if length != 0 * - Private key (256 bytes) * - Signing Private key (20 bytes, or length specified by key certificate) * Total: 663 or more bytes *</pre> * * @author welterde, zzz */ public class PrivateKeyFile { private static final int HASH_EFFORT = VerifiedDestination.MIN_HASHCASH_EFFORT; protected final File file; private final I2PClient client; protected Destination dest; protected PrivateKey privKey; protected SigningPrivateKey signingPrivKey; /** * Create a new PrivateKeyFile, or modify an existing one, with various * types of Certificates. * * Changing a Certificate does not change the public or private keys. * But it does change the Destination Hash, which effectively makes it * a new Destination. In other words, don't change the Certificate on * a Destination you've already registered in a hosts.txt key add form. * * Copied and expanded from that in Destination.java */ public static void main(String args[]) { int hashEffort = HASH_EFFORT; String stype = null; String hostname = null; int mode = 0; boolean error = false; Getopt g = new Getopt("pkf", args, "t:nuxhse:c:a:"); int c; while ((c = g.getopt()) != -1) { switch (c) { case 'c': stype = g.getOptarg(); break; case 't': stype = g.getOptarg(); // fall thru... case 'n': case 'u': case 'x': case 'h': case 's': if (mode == 0) mode = c; else error = true; break; case 'a': hostname = g.getOptarg(); if (mode == 0) mode = c; else error = true; break; case 'e': hashEffort = Integer.parseInt(g.getOptarg()); break; case '?': case ':': default: error = true; break; } // switch } // while int remaining = args.length - g.getOptind(); int reqd = mode == 's' ? 2 : 1; if (error || remaining != reqd) { usage(); System.exit(1); } String filearg = args[g.getOptind()]; I2PClient client = I2PClientFactory.createClient(); try { File f = new File(filearg); boolean exists = f.exists(); PrivateKeyFile pkf = new PrivateKeyFile(f, client); Destination d; if (stype != null) { SigType type = SigType.parseSigType(stype); if (type == null) throw new IllegalArgumentException("Signature type " + stype + " is not supported"); d = pkf.createIfAbsent(type); } else { d = pkf.createIfAbsent(); } if (exists) System.out.println("Original Destination:"); else System.out.println("Created Destination:"); System.out.println(pkf); verifySignature(d); switch (mode) { case 0: // we are done break; case 'n': // Cert constructor generates a null cert pkf.setCertType(Certificate.CERTIFICATE_TYPE_NULL); System.out.println("New destination with null cert is:"); break; case 'u': pkf.setCertType(99); System.out.println("New destination with unknown cert is:"); break; case 'x': pkf.setCertType(Certificate.CERTIFICATE_TYPE_HIDDEN); System.out.println("New destination with hidden cert is:"); break; case 'h': System.out.println("Estimating hashcash generation time, stand by..."); System.out.println(estimateHashCashTime(hashEffort)); pkf.setHashCashCert(hashEffort); System.out.println("New destination with hashcash cert is:"); break; case 's': // Sign dest1 with dest2's Signing Private Key PrivateKeyFile pkf2 = new PrivateKeyFile(args[g.getOptind() + 1]); pkf.setSignedCert(pkf2); System.out.println("New destination with signed cert is:"); break; case 't': // KeyCert SigType type = SigType.parseSigType(stype); if (type == null) throw new IllegalArgumentException("Signature type " + stype + " is not supported"); pkf.setKeyCert(type); System.out.println("New destination with key cert is:"); break; case 'a': // addressbook auth OrderedProperties props = new OrderedProperties(); HostTxtEntry he = new HostTxtEntry(hostname, d.toBase64(), props); he.sign(pkf.getSigningPrivKey()); System.out.println("Addressbook Authentication String:"); OutputStreamWriter out = new OutputStreamWriter(System.out); he.write(out); out.flush(); System.out.println(""); return; default: // shouldn't happen usage(); return; } if (mode != 0) { System.out.println(pkf); pkf.write(); verifySignature(pkf.getDestination()); } } catch (I2PException e) { e.printStackTrace(); System.exit(1); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } private static void usage() { System.err.println("Usage: PrivateKeyFile [-c sigtype] filename (generates if nonexistent, then prints)\n" + " PrivateKeyFile -a example.i2p filename (generate addressbook authentication string)\n" + " PrivateKeyFile -h filename (generates if nonexistent, adds hashcash cert)\n" + " PrivateKeyFile -h -e effort filename (specify HashCash effort instead of default " + HASH_EFFORT + ")\n" + " PrivateKeyFile -n filename (changes to null cert)\n" + " PrivateKeyFile -s filename signwithdestfile (generates if nonexistent, adds cert signed by 2nd dest)\n" + " PrivateKeyFile -t sigtype filename (changes to KeyCertificate of the given sig type)\n" + " PrivateKeyFile -u filename (changes to unknown cert)\n" + " PrivateKeyFile -x filename (changes to hidden cert)\n"); } public PrivateKeyFile(String file) { this(new File(file), I2PClientFactory.createClient()); } public PrivateKeyFile(File file) { this(file, I2PClientFactory.createClient()); } public PrivateKeyFile(File file, I2PClient client) { this.file = file; this.client = client; } /** @since 0.8.9 */ public PrivateKeyFile(File file, I2PSession session) { this(file, session.getMyDestination(), session.getDecryptionKey(), session.getPrivateKey()); } /** * @throws IllegalArgumentException on mismatch of spubkey and spk types * @since 0.8.9 */ public PrivateKeyFile(File file, Destination dest, PrivateKey pk, SigningPrivateKey spk) { if (dest.getSigningPublicKey().getType() != spk.getType()) throw new IllegalArgumentException("Signing key type mismatch"); this.file = file; this.client = null; this.dest = dest; this.privKey = pk; this.signingPrivKey = spk; } /** * @throws IllegalArgumentException on mismatch of spubkey and spk types * @since 0.8.9 */ public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert, PrivateKey pk, SigningPrivateKey spk) { this(file, pubkey, spubkey, cert, pk, spk, null); } /** * @param padding null OK, must be non-null if spubkey length < 128 * @throws IllegalArgumentException on mismatch of spubkey and spk types * @since 0.9.16 */ public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert, PrivateKey pk, SigningPrivateKey spk, byte[] padding) { if (spubkey.getType() != spk.getType()) throw new IllegalArgumentException("Signing key type mismatch"); this.file = file; this.client = null; this.dest = new Destination(); this.dest.setPublicKey(pubkey); this.dest.setSigningPublicKey(spubkey); this.dest.setCertificate(cert); if (padding != null) this.dest.setPadding(padding); this.privKey = pk; this.signingPrivKey = spk; } /** * Can't be used for writing * @since 0.9.26 */ public PrivateKeyFile(InputStream in) throws I2PSessionException { this("/dev/null"); I2PSession s = this.client.createSession(in, new Properties()); this.dest = s.getMyDestination(); this.privKey = s.getDecryptionKey(); this.signingPrivKey = s.getPrivateKey(); } /** * Create with the default signature type if nonexistent. * * Also reads in the file to get the privKey and signingPrivKey, * which aren't available from I2PClient. */ public Destination createIfAbsent() throws I2PException, IOException, DataFormatException { return createIfAbsent(I2PClient.DEFAULT_SIGTYPE); } /** * Create with the specified signature type if nonexistent. * * Also reads in the file to get the privKey and signingPrivKey, * which aren't available from I2PClient. * * @since 0.9.26 */ public Destination createIfAbsent(SigType type) throws I2PException, IOException, DataFormatException { if(!this.file.exists()) { OutputStream out = null; try { out = new SecureFileOutputStream(this.file); if (this.client != null) this.client.createDestination(out, type); else write(); } finally { if (out != null) { try { out.close(); } catch (IOException ioe) {} } } } return getDestination(); } /** * If the destination is not set, read it in from the file. * Also sets the local privKey and signingPrivKey. */ public Destination getDestination() throws I2PSessionException, IOException, DataFormatException { if (dest == null) { I2PSession s = open(); if (s != null) { this.dest = new VerifiedDestination(s.getMyDestination()); this.privKey = s.getDecryptionKey(); this.signingPrivKey = s.getPrivateKey(); } } return this.dest; } public void setDestination(Destination d) { this.dest = d; } /** * Change cert type - caller must also call write(). * Side effect - creates new Destination object. */ public Certificate setCertType(int t) { if (this.dest == null) throw new IllegalArgumentException("Dest is null"); Certificate c = new Certificate(); c.setCertificateType(t); // dests now immutable, must create new Destination newdest = new Destination(); newdest.setPublicKey(dest.getPublicKey()); newdest.setSigningPublicKey(dest.getSigningPublicKey()); newdest.setCertificate(c); dest = newdest; return c; } /** * Change cert type - caller must also call write(). * Side effect - creates new Destination object. * @since 0.9.12 */ public Certificate setKeyCert(SigType type) { if (type == SigType.DSA_SHA1) return setCertType(Certificate.CERTIFICATE_TYPE_NULL); if (dest == null) throw new IllegalArgumentException("Dest is null"); KeyCertificate c = new KeyCertificate(type); SimpleDataStructure signingKeys[]; try { signingKeys = KeyGenerator.getInstance().generateSigningKeys(type); } catch (GeneralSecurityException gse) { throw new RuntimeException("keygen fail", gse); } SigningPublicKey signingPubKey = (SigningPublicKey) signingKeys[0]; signingPrivKey = (SigningPrivateKey) signingKeys[1]; // dests now immutable, must create new Destination newdest = new Destination(); newdest.setPublicKey(dest.getPublicKey()); newdest.setSigningPublicKey(signingPubKey); // fix up key certificate or padding int len = type.getPubkeyLen(); if (len < 128) { byte[] pad = new byte[128 - len]; RandomSource.getInstance().nextBytes(pad); newdest.setPadding(pad); } else if (len > 128) { System.arraycopy(signingPubKey.getData(), 128, c.getPayload(), KeyCertificate.HEADER_LENGTH, len - 128); } newdest.setCertificate(c); dest = newdest; return c; } /** change to hashcash cert - caller must also call write() */ public Certificate setHashCashCert(int effort) { Certificate c = setCertType(Certificate.CERTIFICATE_TYPE_HASHCASH); long begin = System.currentTimeMillis(); System.out.println("Starting hashcash generation now..."); String resource = this.dest.getPublicKey().toBase64() + this.dest.getSigningPublicKey().toBase64(); HashCash hc; try { hc = HashCash.mintCash(resource, effort); } catch (NoSuchAlgorithmException e) { return null; } System.out.println("Generation took: " + DataHelper.formatDuration(System.currentTimeMillis() - begin)); System.out.println("Full Hashcash is: " + hc); // Take the resource out of the stamp String hcs = hc.toString(); int end1 = 0; for (int i = 0; i < 3; i++) { end1 = 1 + hcs.indexOf(':', end1); if (end1 < 0) { System.out.println("Bad hashcash"); return null; } } int start2 = hcs.indexOf(':', end1); if (start2 < 0) { System.out.println("Bad hashcash"); return null; } hcs = hcs.substring(0, end1) + hcs.substring(start2); System.out.println("Short Hashcash is: " + hcs); c.setPayload(DataHelper.getUTF8(hcs)); return c; } /** sign this dest by dest found in pkf2 - caller must also call write() */ public Certificate setSignedCert(PrivateKeyFile pkf2) { Certificate c = setCertType(Certificate.CERTIFICATE_TYPE_SIGNED); Destination d2; try { d2 = pkf2.getDestination(); } catch (I2PException e) { return null; } catch (IOException e) { return null; } if (d2 == null) return null; SigningPrivateKey spk2 = pkf2.getSigningPrivKey(); System.out.println("Signing With Dest:"); System.out.println(pkf2.toString()); int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert byte[] data = new byte[len]; System.arraycopy(this.dest.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); System.arraycopy(this.dest.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES]; Signature sign = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2); if (sign == null) return null; byte[] sig = sign.getData(); System.arraycopy(sig, 0, payload, 0, Signature.SIGNATURE_BYTES); // Add dest2's Hash for reference byte[] h2 = d2.calculateHash().getData(); System.arraycopy(h2, 0, payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH); c.setCertificateType(Certificate.CERTIFICATE_TYPE_SIGNED); c.setPayload(payload); return c; } /** * @return null on error or if not initialized */ public PrivateKey getPrivKey() { try { // call this to force initialization getDestination(); } catch (Exception e) { return null; } return this.privKey; } /** * @return null on error or if not initialized */ public SigningPrivateKey getSigningPrivKey() { try { // call this to force initialization getDestination(); } catch (Exception e) { return null; } return this.signingPrivKey; } public I2PSession open() throws I2PSessionException, IOException { return this.open(new Properties()); } public I2PSession open(Properties opts) throws I2PSessionException, IOException { InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(this.file)); I2PSession s = this.client.createSession(in, opts); return s; } finally { if (in != null) { try { in.close(); } catch (IOException ioe) {} } } } /** * Copied from I2PClientImpl.createDestination() */ public void write() throws IOException, DataFormatException { OutputStream out = null; try { out = new SecureFileOutputStream(this.file); this.dest.writeBytes(out); this.privKey.writeBytes(out); this.signingPrivKey.writeBytes(out); } finally { if (out != null) { try { out.close(); } catch (IOException ioe) {} } } } /** * Verify that the PublicKey matches the PrivateKey, and * the SigningPublicKey matches the SigningPrivateKey. * * @return success * @since 0.9.16 */ public boolean validateKeyPairs() { try { if (!dest.getPublicKey().equals(KeyGenerator.getPublicKey(privKey))) return false; return dest.getSigningPublicKey().equals(KeyGenerator.getSigningPublicKey(signingPrivKey)); } catch (IllegalArgumentException iae) { return false; } } @Override public String toString() { StringBuilder s = new StringBuilder(128); s.append("Dest: "); s.append(this.dest != null ? this.dest.toBase64() : "null"); s.append("\nB32: "); s.append(this.dest != null ? this.dest.toBase32() : "null"); s.append("\nContains: "); s.append(this.dest); s.append("\nPrivate Key: "); s.append(this.privKey); s.append("\nSigining Private Key: "); s.append(this.signingPrivKey); s.append("\n"); return s.toString(); } public static String estimateHashCashTime(int hashEffort) { if (hashEffort <= 0 || hashEffort > 160) return "Bad HashCash value: " + hashEffort; long low = Long.MAX_VALUE; try { low = HashCash.estimateTime(hashEffort); } catch (NoSuchAlgorithmException e) {} // takes a lot longer than the estimate usually... // maybe because the resource string is much longer than used in the estimate? return "It is estimated that generating a HashCash Certificate with value " + hashEffort + " for the Destination will take " + ((low < 1000l * 24l * 60l * 60l * 1000l) ? "approximately " + DataHelper.formatDuration(low) + " to " + DataHelper.formatDuration(4*low) : "longer than three years!" ); } /** * Sample code to verify a 3rd party signature. * This just goes through all the hosts.txt files and tries everybody. * You need to be in the $I2P directory or have a local hosts.txt for this to work. * Doubt this is what you want as it is super-slow, and what good is * a signing scheme where anybody is allowed to sign? * * In a real application you would make a list of approved signers, * do a naming lookup to get their Destinations, and try those only. * Or do a netDb lookup of the Hash in the Certificate, do a reverse * naming lookup to see if it is allowed, then verify the Signature. */ public static boolean verifySignature(Destination d) { if (d.getCertificate().getCertificateType() != Certificate.CERTIFICATE_TYPE_SIGNED) return false; int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert byte[] data = new byte[len]; System.arraycopy(d.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); System.arraycopy(d.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); Signature sig = new Signature(); byte[] payload = d.getCertificate().getPayload(); Hash signerHash = null; if (payload == null) { System.out.println("Bad signed cert - no payload"); return false; } else if (payload.length == Signature.SIGNATURE_BYTES) { sig.setData(payload); } else if (payload.length == Certificate.CERTIFICATE_LENGTH_SIGNED_WITH_HASH) { byte[] pl = new byte[Signature.SIGNATURE_BYTES]; System.arraycopy(payload, 0, pl, 0, Signature.SIGNATURE_BYTES); sig.setData(pl); byte[] hash = new byte[Hash.HASH_LENGTH]; System.arraycopy(payload, Signature.SIGNATURE_BYTES, hash, 0, Hash.HASH_LENGTH); signerHash = new Hash(hash); System.out.println("Destination is signed by " + Base32.encode(hash) + ".b32.i2p"); } else { System.out.println("Bad signed cert - length = " + payload.length); return false; } String[] filenames = new String[] {"privatehosts.txt", "userhosts.txt", "hosts.txt"}; int tried = 0; for (int i = 0; i < filenames.length; i++) { Properties hosts = new Properties(); try { File f = new File(filenames[i]); if ( (f.exists()) && (f.canRead()) ) { DataHelper.loadProps(hosts, f, true); int sz = hosts.size(); if (sz > 0) { tried += sz; if (signerHash == null) System.out.println("Attempting to verify using " + sz + " hosts, this may take a while"); } for (Map.Entry<Object, Object> entry : hosts.entrySet()) { String s = (String) entry.getValue(); Destination signer = new Destination(s); // make it go faster if we have the signerHash hint if (signerHash == null || signer.calculateHash().equals(signerHash)) { if (checkSignature(sig, data, signer.getSigningPublicKey())) { System.out.println("Good signature from: " + entry.getKey()); return true; } if (signerHash != null) { System.out.println("Bad signature from: " + entry.getKey()); // could probably return false here but keep going anyway } } } } } catch (DataFormatException dfe) { } catch (IOException ioe) { } // not found, continue to the next file } if (tried > 0) System.out.println("No valid signer found"); else System.out.println("No addressbooks found to valididate signer"); return false; } public static boolean checkSignature(Signature s, byte[] data, SigningPublicKey spk) { return DSAEngine.getInstance().verifySignature(s, data, spk); } }