diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java index 385c62a2842a9b901f4273c10cb28ed6406da2f7..aaa2ea9a58a1b8155143c5ea2f370eb1dd076ee0 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java @@ -136,19 +136,21 @@ public class Daemon { while (iter.hasNext()) { // yes, the EepGet fetch() is done in next() long start = System.currentTimeMillis(); - AddressBook sub = iter.next(); + AddressBook addressbook = iter.next(); // SubscriptionIterator puts in a dummy AddressBook with no location if no fetch is done - if (DEBUG && log != null && sub.getLocation() != null) { + if (DEBUG && log != null && addressbook.getLocation() != null) { long end = System.currentTimeMillis(); - log.append("Fetch of " + sub.getLocation() + " took " + (end - start)); + log.append("Fetch of " + addressbook.getLocation() + " took " + (end - start)); start = end; } int old = 0, nnew = 0, invalid = 0, conflict = 0, total = 0; - for (Iterator<Map.Entry<String, HostTxtEntry>> eIter = sub.iterator(); eIter.hasNext(); ) { - Map.Entry<String, HostTxtEntry> entry = eIter.next(); + int deleted = 0; + for (Map.Entry<String, HostTxtEntry> entry : addressbook) { + total++; String key = entry.getKey(); boolean isKnown; - Destination oldDest = null; + // NOT set for text file NamingService + Destination oldDest; if (isTextFile) { if (knownNames == null) { // load the hostname set @@ -156,27 +158,34 @@ public class Daemon { opts.setProperty("file", "hosts.txt"); knownNames = router.getNames(opts); } + oldDest = null; isKnown = knownNames.contains(key); } else { oldDest = router.lookup(key); isKnown = oldDest != null; } try { - if (!isKnown) { + HostTxtEntry he = entry.getValue(); + Properties hprops = he.getProps(); + boolean mustValidate = MUST_VALIDATE || hprops != null; + String action = hprops != null ? hprops.getProperty(HostTxtEntry.PROP_ACTION) : null; + if (mustValidate && !he.hasValidSig()) { + if (log != null) { + if (isKnown) + log.append("Bad signature for old key " + key); + else + log.append("Bad signature for new key " + key); + } + invalid++; + } else if (action != null || !isKnown) { if (AddressBook.isValidKey(key)) { - HostTxtEntry he = entry.getValue(); Destination dest = new Destination(he.getDest()); Properties props = new OrderedProperties(); - props.setProperty("s", sub.getLocation()); - if (he.hasValidSig()) { + props.setProperty("s", addressbook.getLocation()); + if (mustValidate) { + // sig checked above props.setProperty("v", "true"); - } else if (MUST_VALIDATE) { - // TODO - //if (log != null) - // log.append("Bad signature for new key " + key); - continue; } - Properties hprops = he.getProps(); if (hprops != null) { // merge in all the received properties for (Map.Entry<Object, Object> e : hprops.entrySet()) { @@ -184,11 +193,394 @@ public class Daemon { props.setProperty(RCVD_PROP_PREFIX + e.getKey(), (String) e.getValue()); } } + if (action != null) { + // Process commands. hprops is non-null. + // Must handle isKnown in each case. + if (action.equals(HostTxtEntry.ACTION_ADDDEST)) { + // Add an alternate destination (new crypto) for existing hostname + // Requires new NamingService support if the key exists + String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST); + if (polddest != null) { + Destination pod = new Destination(polddest); + // fill in oldDest for .txt naming service + if (isKnown && isTextFile) + oldDest = router.lookup(key); + if (pod.equals(dest)) { + // invalid + if (log != null) + log.append("Action: " + action + " failed because" + + " identical old and new destinations for " + key + + " from " + addressbook.getLocation()); + invalid++; + continue; + } else if (!isKnown) { + // we didn't know it before, so we'll add it + } else if (dest.equals(oldDest)) { + // we knew it before, with the same dest + old++; + continue; + } else if (pod.equals(oldDest)) { + // checks out, so verify the inner sig + if (!he.hasValidInnerSig()) { + if (log != null) + log.append("Action: " + action + " failed because" + + " inner signature for key " + key + + " failed" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + // TODO Requires NamingService support + // if (isTextFile), do we replace or not? check sigType.isAvailable() + // router.addAltDest(dest) + if (log != null) + log.append("Action: " + action + " unimplemented" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else { + if (log != null) + log.append("Action: " + action + " failed, missing required parameters"); + invalid++; + continue; + } + } else if (action.equals(HostTxtEntry.ACTION_ADDNAME)) { + // Add an alias for an existing hostname, same dest + if (isKnown) { + // could be same or different dest + old++; + continue; + } + String poldname = hprops.getProperty(HostTxtEntry.PROP_OLDNAME); + if (poldname != null) { + Destination pod = router.lookup(poldname); + if (pod == null) { + // we didn't have the old one, so we'll add the new one + } else if (pod.equals(dest)) { + // checks out, so we'll add the new one + } else { + // mismatch, disallow + if (log != null) + log.append("Action: " + action + " failed because" + + " destination for old name " + poldname + + " does not match" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else { + if (log != null) + log.append("Action: " + action + " failed, missing required parameters" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else if (action.equals(HostTxtEntry.ACTION_ADDSUBDOMAIN)) { + // add a subdomain with verification + if (isKnown) { + old++; + continue; + } + String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST); + String poldname = hprops.getProperty(HostTxtEntry.PROP_OLDNAME); + if (polddest != null && poldname != null) { + // check for valid subdomain + if (!AddressBook.isValidKey(poldname) || + key.indexOf('.' + poldname) <= 0) { + if (log != null) + log.append("Action: " + action + " failed because" + + " old name " + poldname + + " is invalid" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + Destination pod = new Destination(polddest); + Destination pod2 = router.lookup(poldname); + if (pod2 == null) { + // we didn't have the old name + } else if (pod.equals(pod2)) { + // checks out, so verify the inner sig + if (!he.hasValidInnerSig()) { + if (log != null) + log.append("Action: " + action + " failed because" + + " inner signature for old name " + poldname + + " failed" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else { + // mismatch, disallow + if (log != null) + log.append("Action: " + action + " failed because" + + " destination for old name " + poldname + + " does not match provided" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else { + if (log != null) + log.append("Action: " + action + " failed, missing required parameters" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else if (action.equals(HostTxtEntry.ACTION_CHANGEDEST)) { + // change destination on an existing entry + String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST); + if (polddest != null) { + Destination pod = new Destination(polddest); + // fill in oldDest for .txt naming service + if (isKnown && isTextFile) + oldDest = router.lookup(key); + if (!isKnown) { + // we didn't have the old name + } else if (pod.equals(oldDest)) { + // checks out, so verify the inner sig + if (!he.hasValidInnerSig()) { + if (log != null) + log.append("Action: " + action + " failed because" + + " inner signature for key " + key + + " failed" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + // TODO set flag to do non-putifabsent for published below + } else { + // mismatch, disallow + if (log != null) + log.append("Action: " + action + " failed because" + + " destination for key " + key + + " does not match provided" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else { + if (log != null) + log.append("Action: " + action + " failed, missing required parameters" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else if (action.equals(HostTxtEntry.ACTION_CHANGENAME)) { + // Delete old name, replace with new + if (isKnown) { + old++; + continue; + } + String poldname = hprops.getProperty(HostTxtEntry.PROP_OLDNAME); + if (poldname != null) { + Destination pod = router.lookup(poldname); + if (pod == null) { + // we didn't have the old name + } else if (pod.equals(dest)) { + // checks out, so we'll delete it + if (knownNames != null) + knownNames.remove(poldname); + boolean success = router.remove(poldname); + if (success) + deleted++; + if (log != null) { + if (success) + log.append("Removed: " + poldname + + " to be replaced with " + key + + " from " + addressbook.getLocation()); + else + log.append("Remove failed for: " + poldname + + " to be replaced with " + key + + " from " + addressbook.getLocation()); + } + // now update the published addressbook + if (published != null) { + if (publishedNS == null) + publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); + success = publishedNS.remove(poldname); + if (log != null && !success) + log.append("Remove from published address book " + published.getAbsolutePath() + " failed for " + poldname); + } + } else { + // mismatch, disallow + if (log != null) + log.append("Action: " + action + " failed because" + + " destination for old name " + poldname + + " does not match new name " + key + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else { + if (log != null) + log.append("Action: " + action + " failed, missing required parameters" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } else if (action.equals(HostTxtEntry.ACTION_REMOVE)) { + // FIXME can't get here, no key or dest + // delete this entry + if (!isKnown) { + old++; + continue; + } + String polddest = hprops.getProperty(HostTxtEntry.PROP_DEST); + String poldname = hprops.getProperty(HostTxtEntry.PROP_NAME); + if (polddest != null && poldname != null) { + Destination pod = new Destination(polddest); + Destination pod2 = router.lookup(poldname); + if (pod.equals(pod2)) { + if (knownNames != null) + knownNames.remove(poldname); + boolean success = router.remove(poldname); + if (success) + deleted++; + if (log != null) { + if (success) + log.append("Removed: " + poldname + + " as requested" + + " from " + addressbook.getLocation()); + else + log.append("Remove failed for: " + poldname + + " as requested" + + " from " + addressbook.getLocation()); + } + // now update the published addressbook + if (published != null) { + if (publishedNS == null) + publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); + success = publishedNS.remove(poldname); + if (log != null && !success) + log.append("Remove from published address book " + published.getAbsolutePath() + " failed for " + poldname); + } + } else if (pod2 != null) { + // mismatch, disallow + if (log != null) + log.append("Action: " + action + " failed because" + + " destination for " + poldname + + " does not match" + + " from " + addressbook.getLocation()); + invalid++; + } + } else { + if (log != null) + log.append("Action: " + action + " failed, missing required parameters" + + " from " + addressbook.getLocation()); + invalid++; + } + continue; + } else if (action.equals(HostTxtEntry.ACTION_REMOVEALL)) { + // FIXME can't get here, no key or dest + // delete all entries with this destination + if (!isKnown) { + old++; + continue; + } + String polddest = hprops.getProperty(HostTxtEntry.PROP_DEST); + // oldname is optional, but nice because not all books support reverse lookup + if (polddest != null) { + Destination pod = new Destination(polddest); + String poldname = hprops.getProperty(HostTxtEntry.PROP_NAME); + if (poldname != null) { + Destination pod2 = router.lookup(poldname); + if (pod.equals(pod2)) { + if (knownNames != null) + knownNames.remove(poldname); + boolean success = router.remove(poldname); + if (success) + deleted++; + if (log != null) { + if (success) + log.append("Removed: " + poldname + + " as requested" + + " from " + addressbook.getLocation()); + else + log.append("Remove failed for: " + poldname + + " as requested" + + " from " + addressbook.getLocation()); + } + // now update the published addressbook + if (published != null) { + if (publishedNS == null) + publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); + success = publishedNS.remove(poldname); + if (log != null && !success) + log.append("Remove from published address book " + published.getAbsolutePath() + " failed for " + poldname); + } + } else if (pod2 != null) { + // mismatch, disallow + if (log != null) + log.append("Action: " + action + " failed because" + + " destination for " + poldname + + " does not match" + + " from " + addressbook.getLocation()); + invalid++; + } + } + // reverse lookup, delete all + // There's no NamingService API to get a list of all reverse + String rev; + String rev2 = null; + while ((rev = router.reverseLookup(pod)) != null) { + // prevent getting stuck from buggy NS + if (rev.equals(rev2)) + break; + rev2 = rev; + // forward check in case hash collision or something + Destination fwd = router.lookup(rev); + if (!pod.equals(fwd)) + break; // can't go around again, fail + if (knownNames != null) + knownNames.remove(rev); + boolean success = router.remove(rev); + if (success) + deleted++; + if (log != null) { + if (success) + log.append("Removed: " + rev + + " as requested" + + " from " + addressbook.getLocation()); + else + log.append("Remove failed for: " + rev + + " as requested" + + " from " + addressbook.getLocation()); + } + // now update the published addressbook + if (published != null) { + if (publishedNS == null) + publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); + success = publishedNS.remove(rev); + if (log != null && !success) + log.append("Remove from published address book " + published.getAbsolutePath() + " failed for " + rev); + } + } + } else { + if (log != null) + log.append("Action: " + action + " failed, missing required parameters" + + " from " + addressbook.getLocation()); + invalid++; + } + continue; + } else if (action.equals(HostTxtEntry.ACTION_UPDATE)) { + if (isKnown) { + // TODO set flag to do non-putifabsent for published below + } + } else { + if (log != null) + log.append("Action: " + action + " unrecognized" + + " from " + addressbook.getLocation()); + invalid++; + continue; + } + } boolean success = router.put(key, dest, props); if (log != null) { if (success) log.append("New address " + key + - " added to address book. From: " + sub.getLocation()); + " added to address book. From: " + addressbook.getLocation()); else log.append("Save to naming service " + router + " failed for new key " + key); } @@ -198,9 +590,7 @@ public class Daemon { publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); success = publishedNS.putIfAbsent(key, dest, props); if (log != null && !success) { - try { - log.append("Save to published address book " + published.getCanonicalPath() + " failed for new key " + key); - } catch (IOException ioe) {} + log.append("Save to published address book " + published.getAbsolutePath() + " failed for new key " + key); } } if (isTextFile) @@ -209,43 +599,45 @@ public class Daemon { nnew++; } else if (log != null) { log.append("Bad hostname " + key + " from " - + sub.getLocation()); + + addressbook.getLocation()); invalid++; } + /**** } else if (false && DEBUG && log != null) { // lookup the conflict if we haven't yet (O(n**2) for text file) if (isTextFile) oldDest = router.lookup(key); if (oldDest != null && !oldDest.toBase64().equals(entry.getValue())) { log.append("Conflict for " + key + " from " - + sub.getLocation() + + addressbook.getLocation() + ". Destination in remote address book is " + entry.getValue()); conflict++; } else { old++; } + ****/ } else { old++; } } catch (DataFormatException dfe) { if (log != null) - log.append("Invalid b64 for " + key + " From: " + sub.getLocation()); + log.append("Invalid b64 for " + key + " From: " + addressbook.getLocation()); invalid++; } - total++; } if (DEBUG && log != null && total > 0) { - log.append("Merge of " + sub.getLocation() + " into " + router + + log.append("Merge of " + addressbook.getLocation() + " into " + router + " took " + (System.currentTimeMillis() - start) + " ms with " + total + " total, " + nnew + " new, " + old + " old, " + + deleted + " deleted, " + invalid + " invalid, " + conflict + " conflicts"); - } - sub.delete(); - } + } // entries + addressbook.delete(); + } // subscriptions subscriptions.write(); } diff --git a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java index 1540e36c369944572d8bee8de138290c03a35fce..f1934cc3c76512b581246af3b17f512282e9acda 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java @@ -138,6 +138,9 @@ class HostTxtEntry { out.newLine(); } + /** + * Verify with the dest public key using the "sig" property + */ public boolean hasValidSig() { if (props == null) return false; @@ -195,6 +198,76 @@ class HostTxtEntry { return isValid; } + /** + * Verify with the "olddest" property's public key using the "oldsig" property + */ + public boolean hasValidInnerSig() { + if (props == null) + return false; + boolean rv = false; + // don't cache result + if (true) { + StringBuilder buf = new StringBuilder(1024); + String sig = null; + String olddest = null; + buf.append(name); + buf.append(KV_SEPARATOR); + buf.append(dest); + boolean started = false; + for (Map.Entry<Object, Object> e : props.entrySet()) { + String k = (String) e.getKey(); + String v = (String) e.getValue(); + if (k.equals(PROP_SIG)) { + continue; + } + if (k.equals(PROP_OLDSIG)) { + if (sig != null) + return false; + sig = v; + // remove from the written data + continue; + } + if (k.equals(PROP_OLDDEST)) { + if (olddest != null) + return false; + olddest = v; + } + if (started) { + buf.append(PROP_SEPARATOR); + } else { + started = true; + buf.append(PROPS_SEPARATOR); + } + buf.append(k); + buf.append(KV_SEPARATOR); + buf.append(v); + } + if (sig == null || olddest == null) + return false; + byte[] sdata = Base64.decode(sig); + if (sdata == null) + return false; + Destination d; + try { + d = new Destination(olddest); + } catch (DataFormatException dfe) { + return false; + } + SigningPublicKey spk = d.getSigningPublicKey(); + SigType type = spk.getType(); + if (type == null) + return false; + Signature s; + try { + s = new Signature(type, sdata); + } catch (IllegalArgumentException iae) { + return false; + } + rv = DSAEngine.getInstance().verifySignature(s, DataHelper.getUTF8(buf.toString()), spk); + } + return rv; + } + @Override public int hashCode() { return dest.hashCode(); @@ -213,16 +286,29 @@ class HostTxtEntry { return dest.equals(he.getDest()); } - /** for testing only */ + /** + * Sign and set the "sig" property + * for testing only + */ private void sign(SigningPrivateKey spk) { + signIt(spk, PROP_SIG); + } + + /** + * Sign and set the "oldsig" property + * for testing only + */ + private void signInner(SigningPrivateKey spk) { + signIt(spk, PROP_OLDSIG); + } + + /** + * for testing only + * @param sigprop The signature property to set + */ + private void signIt(SigningPrivateKey spk, String sigprop) { if (props == null) throw new IllegalStateException(); - Destination d; - try { - d = new Destination(dest); - } catch (DataFormatException dfe) { - throw new IllegalStateException("bah", dfe); - } StringBuilder buf = new StringBuilder(1024); buf.append(name); buf.append(KV_SEPARATOR); @@ -231,7 +317,7 @@ class HostTxtEntry { for (Map.Entry<Object, Object> e : props.entrySet()) { String k = (String) e.getKey(); String v = (String) e.getValue(); - if (k.equals(PROP_SIG)) + if (k.equals(sigprop)) throw new IllegalStateException(); if (started) { buf.append(PROP_SEPARATOR); @@ -246,26 +332,37 @@ class HostTxtEntry { Signature s = DSAEngine.getInstance().sign(DataHelper.getUTF8(buf.toString()), spk); if (s == null) throw new IllegalArgumentException("sig failed"); - props.setProperty(PROP_SIG, s.toBase64()); + props.setProperty(sigprop, s.toBase64()); } public static void main(String[] args) throws Exception { + // outer File f = new File("tmp-eepPriv.dat"); + // inner + File f2 = new File("tmp-eepPriv2.dat"); PrivateKeyFile pkf = new PrivateKeyFile(f); + PrivateKeyFile pkf2 = new PrivateKeyFile(f2); pkf.createIfAbsent(SigType.EdDSA_SHA512_Ed25519); + pkf2.createIfAbsent(SigType.DSA_SHA1); OrderedProperties props = new OrderedProperties(); props.setProperty("c", "ccccccccccc"); props.setProperty("a", "aaaa"); + props.setProperty(PROP_OLDDEST, pkf2.getDestination().toBase64()); HostTxtEntry he = new HostTxtEntry("foo.i2p", pkf.getDestination().toBase64(), props); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); out.write("Before signing:\n"); he.write(out); out.flush(); SigningPrivateKey priv = pkf.getSigningPrivKey(); + SigningPrivateKey priv2 = pkf2.getSigningPrivKey(); + he.signInner(priv2); + out.write("After signing inner:\n"); + he.write(out); he.sign(priv); out.write("After signing:\n"); he.write(out); out.flush(); + System.out.println("Orig has valid inner sig? " + he.hasValidInnerSig()); System.out.println("Orig has valid sig? " + he.hasValidSig()); // now create 2nd, read in StringWriter sw = new StringWriter(1024); @@ -275,7 +372,9 @@ class HostTxtEntry { String line = sw.toString(); line = line.substring(line.indexOf(PROPS_SEPARATOR) + 2); HostTxtEntry he2 = new HostTxtEntry("foo.i2p", pkf.getDestination().toBase64(), line); + System.out.println("Dupl. has valid inner sig? " + he2.hasValidInnerSig()); System.out.println("Dupl. has valid sig? " + he2.hasValidSig()); f.delete(); + f2.delete(); } }