From 4358d111912e8722bda2b31e53ad183dda430b4f Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 23 Apr 2013 18:19:49 +0000 Subject: [PATCH 1/3] Baseline checkin of DigestAuthenticator from Jetty 7.6.10 before mods --- .../net/i2p/jetty/I2PDigestAuthenticator.java | 386 ++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java diff --git a/apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java b/apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java new file mode 100644 index 000000000..8b5882694 --- /dev/null +++ b/apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java @@ -0,0 +1,386 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security.authentication; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.security.SecurityHandler; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Authentication.User; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.util.security.Credential; + +/** + * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $ + * + * The nonce max age in ms can be set with the {@link SecurityHandler#setInitParameter(String, String)} + * using the name "maxNonceAge" + */ +public class DigestAuthenticator extends LoginAuthenticator +{ + private static final Logger LOG = Log.getLogger(DigestAuthenticator.class); + SecureRandom _random = new SecureRandom(); + private long _maxNonceAgeMs = 60*1000; + private ConcurrentMap _nonceCount = new ConcurrentHashMap(); + private Queue _nonceQueue = new ConcurrentLinkedQueue(); + private static class Nonce + { + final String _nonce; + final long _ts; + AtomicInteger _nc=new AtomicInteger(); + public Nonce(String nonce, long ts) + { + _nonce=nonce; + _ts=ts; + } + } + + /* ------------------------------------------------------------ */ + public DigestAuthenticator() + { + super(); + } + + /* ------------------------------------------------------------ */ + /** + * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration) + */ + @Override + public void setConfiguration(AuthConfiguration configuration) + { + super.setConfiguration(configuration); + + String mna=configuration.getInitParameter("maxNonceAge"); + if (mna!=null) + { + synchronized (this) + { + _maxNonceAgeMs=Long.valueOf(mna); + } + } + } + + /* ------------------------------------------------------------ */ + public synchronized void setMaxNonceAge(long maxNonceAgeInMillis) + { + _maxNonceAgeMs = maxNonceAgeInMillis; + } + + /* ------------------------------------------------------------ */ + public String getAuthMethod() + { + return Constraint.__DIGEST_AUTH; + } + + /* ------------------------------------------------------------ */ + public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException + { + return true; + } + + /* ------------------------------------------------------------ */ + public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException + { + if (!mandatory) + return new DeferredAuthentication(this); + + HttpServletRequest request = (HttpServletRequest)req; + HttpServletResponse response = (HttpServletResponse)res; + String credentials = request.getHeader(HttpHeaders.AUTHORIZATION); + + try + { + boolean stale = false; + if (credentials != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("Credentials: " + credentials); + QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false); + final Digest digest = new Digest(request.getMethod()); + String last = null; + String name = null; + + while (tokenizer.hasMoreTokens()) + { + String tok = tokenizer.nextToken(); + char c = (tok.length() == 1) ? tok.charAt(0) : '\0'; + + switch (c) + { + case '=': + name = last; + last = tok; + break; + case ',': + name = null; + break; + case ' ': + break; + + default: + last = tok; + if (name != null) + { + if ("username".equalsIgnoreCase(name)) + digest.username = tok; + else if ("realm".equalsIgnoreCase(name)) + digest.realm = tok; + else if ("nonce".equalsIgnoreCase(name)) + digest.nonce = tok; + else if ("nc".equalsIgnoreCase(name)) + digest.nc = tok; + else if ("cnonce".equalsIgnoreCase(name)) + digest.cnonce = tok; + else if ("qop".equalsIgnoreCase(name)) + digest.qop = tok; + else if ("uri".equalsIgnoreCase(name)) + digest.uri = tok; + else if ("response".equalsIgnoreCase(name)) + digest.response = tok; + name=null; + } + } + } + + int n = checkNonce(digest,(Request)request); + + if (n > 0) + { + UserIdentity user = _loginService.login(digest.username,digest); + if (user!=null) + { + renewSession(request,response); + return new UserAuthentication(getAuthMethod(),user); + } + } + else if (n == 0) + stale = true; + + } + + if (!DeferredAuthentication.isDeferred(response)) + { + String domain = request.getContextPath(); + if (domain == null) + domain = "/"; + response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + _loginService.getName() + + "\", domain=\"" + + domain + + "\", nonce=\"" + + newNonce((Request)request) + + "\", algorithm=MD5, qop=\"auth\"," + + " stale=" + stale); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + + return Authentication.SEND_CONTINUE; + } + + return Authentication.UNAUTHENTICATED; + } + catch (IOException e) + { + throw new ServerAuthException(e); + } + + } + + /* ------------------------------------------------------------ */ + public String newNonce(Request request) + { + Nonce nonce; + + do + { + byte[] nounce = new byte[24]; + _random.nextBytes(nounce); + + nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp()); + } + while (_nonceCount.putIfAbsent(nonce._nonce,nonce)!=null); + _nonceQueue.add(nonce); + + return nonce._nonce; + } + + /** + * @param nstring nonce to check + * @param request + * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce + */ + /* ------------------------------------------------------------ */ + private int checkNonce(Digest digest, Request request) + { + // firstly let's expire old nonces + long expired; + synchronized (this) + { + expired = request.getTimeStamp()-_maxNonceAgeMs; + } + + Nonce nonce=_nonceQueue.peek(); + while (nonce!=null && nonce._tsInteger.MAX_VALUE) + return 0; + int old=nonce._nc.get(); + while (!nonce._nc.compareAndSet(old,(int)count)) + old=nonce._nc.get(); + if (count<=old) + return -1; + + return 1; + } + catch (Exception e) + { + LOG.ignore(e); + } + return -1; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private static class Digest extends Credential + { + private static final long serialVersionUID = -2484639019549527724L; + final String method; + String username = ""; + String realm = ""; + String nonce = ""; + String nc = ""; + String cnonce = ""; + String qop = ""; + String uri = ""; + String response = ""; + + /* ------------------------------------------------------------ */ + Digest(String m) + { + method = m; + } + + /* ------------------------------------------------------------ */ + @Override + public boolean check(Object credentials) + { + if (credentials instanceof char[]) + credentials=new String((char[])credentials); + String password = (credentials instanceof String) ? (String) credentials : credentials.toString(); + + try + { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] ha1; + if (credentials instanceof Credential.MD5) + { + // Credentials are already a MD5 digest - assume it's in + // form user:realm:password (we have no way to know since + // it's a digest, alright?) + ha1 = ((Credential.MD5) credentials).getDigest(); + } + else + { + // calc A1 digest + md.update(username.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(realm.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(password.getBytes(StringUtil.__ISO_8859_1)); + ha1 = md.digest(); + } + // calc A2 digest + md.reset(); + md.update(method.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(uri.getBytes(StringUtil.__ISO_8859_1)); + byte[] ha2 = md.digest(); + + // calc digest + // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" + // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) + // <"> + // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) + // ) > <"> + + md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(nonce.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(nc.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(cnonce.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(qop.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1)); + byte[] digest = md.digest(); + + // check digest + return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response)); + } + catch (Exception e) + { + LOG.warn(e); + } + + return false; + } + + public String toString() + { + return username + "," + response; + } + } +} From 22025b0c3a1d0f861df01b581fde74781884336c Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 23 Apr 2013 18:22:48 +0000 Subject: [PATCH 2/3] * Console: Fix Jetty digest auth bug causing repeated password requests I2P fixes for out-of-order nonce counts. Based on DigestAuthenticator in Jetty 7.6.10. Includes the nonce count verification code from Tomcat 7.0.35. ref: http://jira.codehaus.org/browse/JETTY-1468 which was closed not-a-bug. ref: https://bugs.eclipse.org/bugs/show_bug.cgi?id=336443 in which the Jetty implementation was introduced. --- apps/jetty/build.xml | 3 +- .../net/i2p/jetty/I2PDigestAuthenticator.java | 130 ++++++++++++------ .../i2p/router/web/RouterConsoleRunner.java | 7 +- 3 files changed, 97 insertions(+), 43 deletions(-) diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml index 97bb3abe1..940d82e6e 100644 --- a/apps/jetty/build.xml +++ b/apps/jetty/build.xml @@ -202,6 +202,7 @@ + @@ -216,7 +217,7 @@ debug="true" deprecation="on" source="1.5" target="1.5" destdir="./build/obj" includeAntRuntime="false" - classpath="../../core/java/build/i2p.jar:./jettylib/commons-logging.jar:./jettylib/javax.servlet.jar:./jettylib/org.mortbay.jetty.jar:./jettylib/jetty-http.jar:./jettylib/jetty-io.jar:./jettylib/jetty-util.jar:./jettylib/jetty-xml.jar" > + classpath="../../core/java/build/i2p.jar:./jettylib/commons-logging.jar:./jettylib/javax.servlet.jar:./jettylib/org.mortbay.jetty.jar:./jettylib/jetty-http.jar:./jettylib/jetty-io.jar:./jettylib/jetty-security.jar:./jettylib/jetty-util.jar:./jettylib/jetty-xml.jar" > diff --git a/apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java b/apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java index 8b5882694..f749effe1 100644 --- a/apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java +++ b/apps/jetty/java/src/net/i2p/jetty/I2PDigestAuthenticator.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.security.authentication; +package net.i2p.jetty; import java.io.IOException; import java.security.MessageDigest; @@ -25,7 +25,6 @@ import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -36,6 +35,8 @@ import org.eclipse.jetty.http.HttpHeaders; import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.security.ServerAuthException; import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.security.authentication.DeferredAuthentication; +import org.eclipse.jetty.security.authentication.DigestAuthenticator; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication.User; import org.eclipse.jetty.server.Request; @@ -50,74 +51,110 @@ import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Credential; /** + * I2P fixes for out-of-order nonce counts. + * Based on DigestAuthenticator in Jetty 7.6.10. + * Includes the nonce count verification code from Tomcat 7.0.35. + * ref: http://jira.codehaus.org/browse/JETTY-1468 which was closed not-a-bug. + * ref: https://bugs.eclipse.org/bugs/show_bug.cgi?id=336443 in which the + * Jetty implementation was introduced. + * + * @since 0.9.6 + * * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $ * * The nonce max age in ms can be set with the {@link SecurityHandler#setInitParameter(String, String)} * using the name "maxNonceAge" */ -public class DigestAuthenticator extends LoginAuthenticator +public class I2PDigestAuthenticator extends DigestAuthenticator { - private static final Logger LOG = Log.getLogger(DigestAuthenticator.class); + // shadows super + private static final Logger LOG = Log.getLogger(I2PDigestAuthenticator.class); SecureRandom _random = new SecureRandom(); - private long _maxNonceAgeMs = 60*1000; + // shadows super + private long _maxNonceAgeMs = 60*60*1000L; private ConcurrentMap _nonceCount = new ConcurrentHashMap(); + // shadows super private Queue _nonceQueue = new ConcurrentLinkedQueue(); + + /* + * Shadows super + * + * Contains code from Tomcat 7.0.35 DigestAuthenticator.NonceInfo + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ private static class Nonce { final String _nonce; final long _ts; - AtomicInteger _nc=new AtomicInteger(); + private volatile boolean seen[]; + private volatile int offset; + private volatile int count = 0; + private static final int seenWindowSize = 100; + public Nonce(String nonce, long ts) { _nonce=nonce; _ts=ts; + seen = new boolean[seenWindowSize]; + offset = seenWindowSize / 2; + } + + public synchronized boolean nonceCountValid(long nonceCount) { + if ((count - offset) >= nonceCount || + (nonceCount > count - offset + seen.length)) { + return false; + } + int checkIndex = (int) ((nonceCount + offset) % seen.length); + if (seen[checkIndex]) { + return false; + } else { + seen[checkIndex] = true; + seen[count % seen.length] = false; + count++; + return true; + } } } /* ------------------------------------------------------------ */ - public DigestAuthenticator() + public I2PDigestAuthenticator() { super(); } - /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration) - */ - @Override - public void setConfiguration(AuthConfiguration configuration) - { - super.setConfiguration(configuration); - - String mna=configuration.getInitParameter("maxNonceAge"); - if (mna!=null) - { - synchronized (this) - { - _maxNonceAgeMs=Long.valueOf(mna); - } - } - } /* ------------------------------------------------------------ */ + + /** + * Store local copy since field in super is private + */ + @Override public synchronized void setMaxNonceAge(long maxNonceAgeInMillis) { + super.setMaxNonceAge(maxNonceAgeInMillis); _maxNonceAgeMs = maxNonceAgeInMillis; } /* ------------------------------------------------------------ */ - public String getAuthMethod() - { - return Constraint.__DIGEST_AUTH; - } - /* ------------------------------------------------------------ */ - public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException - { - return true; - } - - /* ------------------------------------------------------------ */ + /** + * No changes from super + */ + @Override public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException { if (!mandatory) @@ -224,6 +261,11 @@ public class DigestAuthenticator extends LoginAuthenticator } /* ------------------------------------------------------------ */ + + /** + * No changes from super + */ + @Override public String newNonce(Request request) { Nonce nonce; @@ -247,6 +289,10 @@ public class DigestAuthenticator extends LoginAuthenticator * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce */ /* ------------------------------------------------------------ */ + + /** + * Contains fixes + */ private int checkNonce(Digest digest, Request request) { // firstly let's expire old nonces @@ -274,12 +320,9 @@ public class DigestAuthenticator extends LoginAuthenticator long count = Long.parseLong(digest.nc,16); if (count>Integer.MAX_VALUE) return 0; - int old=nonce._nc.get(); - while (!nonce._nc.compareAndSet(old,(int)count)) - old=nonce._nc.get(); - if (count<=old) + if (!nonce.nonceCountValid(count)) { return -1; - + } return 1; } catch (Exception e) @@ -292,6 +335,11 @@ public class DigestAuthenticator extends LoginAuthenticator /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ + + /** + * Shadows super. + * No changes from super + */ private static class Digest extends Credential { private static final long serialVersionUID = -2484639019549527724L; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java index dd481d48b..069819963 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -29,6 +29,7 @@ import static net.i2p.app.ClientAppState.*; import net.i2p.apps.systray.SysTray; import net.i2p.data.Base32; import net.i2p.data.DataHelper; +import net.i2p.jetty.I2PDigestAuthenticator; import net.i2p.jetty.I2PLogger; import net.i2p.router.RouterContext; import net.i2p.router.update.ConsoleUpdateManager; @@ -105,7 +106,11 @@ public class RouterConsoleRunner implements RouterApp { private static final String DEFAULT_WEBAPP_CONFIG_FILENAME = "webapps.config"; // Jetty Auth - private static final DigestAuthenticator authenticator = new DigestAuthenticator(); + private static final DigestAuthenticator authenticator = new I2PDigestAuthenticator(); + static { + // default changed from 0 (forever) in Jetty 6 to 60*1000 ms in Jetty 7 + authenticator.setMaxNonceAge(7*24*60*60*1000L); + } public static final String JETTY_REALM = "i2prouter"; private static final String JETTY_ROLE = "routerAdmin"; public static final String PROP_CONSOLE_PW = "routerconsole.auth." + JETTY_REALM; From aa547a161085f35d7c9f2627a648ce705ee24fe3 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 23 Apr 2013 18:23:38 +0000 Subject: [PATCH 3/3] * i2ptunnel: Block b32.i2p supercookies --- .../net/i2p/i2ptunnel/HTTPResponseOutputStream.java | 11 +++++++++++ history.txt | 4 ++++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java index 73dba50da..f1b73c114 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java @@ -193,6 +193,17 @@ class HTTPResponseOutputStream extends FilterOutputStream { } else if ("content-type".equals(lcKey)) { // save for compress decision on server side _contentType = val; + } else if ("set-cookie".equals(lcKey)) { + String lcVal = val.toLowerCase(Locale.US); + if (lcVal.contains("domain=b32.i2p") || + lcVal.contains("domain=.b32.i2p")) { + // Strip privacy-damaging "supercookie" for b32.i2p + // Let's presume the user agent ignores a cookie for "i2p" + // See RFC 6265 and http://publicsuffix.org/ + if (_log.shouldLog(Log.INFO)) + _log.info("Stripping \"" + key + ": " + val + "\" from response "); + break; + } } out.write((key.trim() + ": " + val.trim() + "\r\n").getBytes()); } diff --git a/history.txt b/history.txt index 0f78c62ff..6fde6cb7e 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,7 @@ +2013-04-23 zzz + * Console: Fix Jetty digest auth bug causing repeated password requests + * i2ptunnel: Block b32.i2p supercookies + 2013-04-21 zzz * AppManager: Add HTML debug output * Installer: Fix installations to a different drive on Windows diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index e36cd3b14..7f4edb975 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 12; + public final static long BUILD = 13; /** for example "-test" */ public final static String EXTRA = "";