In case anyone else finds themselves trying to get a java or scala app to coexist with a rails app, I hacked up the following. Its in scala but uses java apis so should be easy to read. As far as I can tell it replicates Devise's behavior, and if I hit the confirmation link in the rails app with the raw token rails/devise generates the same encoded string.
import java.security.spec.KeySpec
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
import javax.xml.bind.DatatypeConverter
import java.util.Base64 
// copy functionality from Rails Devise
object TokenGenerator {
  // sample values 9exithzwZ8P9meqdVs3K => 54364224169895883e87c8412be5874039b470e26e762cb3ddc37c0bdcf014f5
  //              5zNMi6egbyPoDUy2t3NY => 75bd5d53aa36d3fc61ac186b4c6e2be8353e6b39536d3cf846719284e05474ca
  private val deviseSecret = sys.env("DEVISE_SECRET")
  private val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
  val encoder = Base64.getUrlEncoder()
  case class TokenInfo(raw: String, encoded: String)
  def createConfirmationToken: TokenInfo = {
    // copy behavior from rails world. Don't know why it does this
    val replacements = Map('l' -> "s", 'I' -> "x", 'O' -> "y", '0' -> "z")
    // make a raw key of 20 chars, doesn't seem to matter what they are, just need url valid set
    val bytes = new Array[Byte](16)
    scala.util.Random.nextBytes(bytes)
    val raw = encoder.encodeToString(bytes).take(20).foldLeft(""){(acc, x) => acc ++ replacements.get(x).getOrElse(x.toString)}
    TokenInfo(raw, digestForConfirmationToken(raw))
  }
  private def generateKey(salt: String): Array[Byte] = {
    val iter = 65536
    val keySize = 512
    val spec = new PBEKeySpec(deviseSecret.toCharArray, salt.getBytes("UTF-8"), iter, keySize)
    val sk = factory.generateSecret(spec)
    val skspec = new SecretKeySpec(sk.getEncoded, "AES")
    skspec.getEncoded
  }
  def sha256HexDigest(s: String, key: Array[Byte]): String = {
    val mac = Mac.getInstance("HmacSHA256")
    val keySpec = new SecretKeySpec(key, "RAW")
    mac.init(keySpec)
    val result: Array[Byte] = mac.doFinal(s.getBytes())
    DatatypeConverter.printHexBinary(result).toLowerCase
  }
  private def getDigest(raw: String, salt: String) = sha256HexDigest(raw, generateKey(salt))
  // devise uses salt "Devise #{column}", in this case its confirmation_token
  def digestForConfirmationToken(raw: String) = getDigest(raw, "Devise confirmation_token")
}