All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.plasmalabs.crypto.generation.mnemonic.Phrase.scala Maven / Gradle / Ivy

The newest version!
package org.plasmalabs.crypto.generation.mnemonic

import cats.implicits._
import org.plasmalabs.crypto.generation.mnemonic.Language.LanguageWordList
import org.plasmalabs.crypto.hash.sha256

/**
 * A mnemonic phrase of words from a valid language list.
 *
 * @param value the sequence of words in the phrase
 */
case class Phrase(value: IndexedSeq[String], size: MnemonicSize, languageWords: LanguageWordList)

object Phrase {

  /**
   * Instantiates a `Phrase` from a `String` containing a list of words separated by white-space.
   *
   * @param words the words of the mnemonic phrase
   * @param language the word list in a particular language
   * @return either a `ValidationFailure` if the mnemonic phrase is invalid or a `Phrase` if it is valid
   */
  def validated(
    words:    String,
    language: Language
  ): Either[PhraseFailure, Phrase] =
    for {
      size     <- sizeFromNumberOfWords(words.split(" ").count(_.nonEmpty))
      wordList <- LanguageWordList.validated(language).leftMap(PhraseFailures.WordListFailure.apply)
      phrase   <- Right(Phrase(words.toLowerCase.split("\\s+").map(_.trim).toIndexedSeq, size, wordList))
      _        <- Either.cond(phrase.value.length == size.wordLength, phrase, PhraseFailures.InvalidWordLength)
      _        <- Either.cond(phrase.value.forall(wordList.value.contains), phrase, PhraseFailures.InvalidWords)
      (entropyBinaryString, checksumFromPhrase) = toBinaryString(phrase)
      checksumFromSha256: String = calculateChecksum(entropyBinaryString, size)
      _ <- Either.cond(checksumFromPhrase == checksumFromSha256, phrase, PhraseFailures.InvalidChecksum)
    } yield phrase

  def fromEntropy(
    entropy:  Entropy,
    size:     MnemonicSize,
    language: Language
  ): Either[PhraseFailure, Phrase] = for {
    _ <- Either.cond(
      entropy.value.length == size.entropyLength / byteLen,
      entropy,
      PhraseFailures.InvalidEntropyLength
    )
    wordList <- LanguageWordList.validated(language).leftMap(PhraseFailures.WordListFailure.apply)
    entropyBinaryString = entropy.value.toArray.map(byteTo8BitString).mkString
    checksum = calculateChecksum(entropyBinaryString, size)
    phrase = fromBinaryString(entropyBinaryString ++ checksum, size, wordList)
  } yield phrase

  // the phrase converted to binary with each word being 11 bits
  // 1. get index of word in wordlist
  // 2.  map indices to 11 bit representation (return List[String])
  // 3. concatenate the strings together to make a long binary string
  // 4. slice the string into the entropy + checksum pieces
  private[mnemonic] def toBinaryString(phrase: Phrase): (String, String) =
    phrase.value
      .map(phrase.languageWords.value.indexOf(_))
      .map(intTo11BitString)
      .mkString
      .splitAt(phrase.size.entropyLength)

  // the phrase converted to binary with each word being 11 bits
  // 1. get index of word in wordlist
  // 2.  map indices to 11 bit representation (return List[String])
  // 3. concatenate the strings together to make a long binary string
  // 4. slice the string into the entropy + checksum pieces
  private[mnemonic] def fromBinaryString(
    phraseBinaryString: String,
    size:               MnemonicSize,
    languageWords:      LanguageWordList
  ): Phrase = {
    val phraseWords = phraseBinaryString
      .grouped(indexLen)
      .map(Integer.parseInt(_, 2))
      .map(languageWords.value(_))
      .toIndexedSeq

    Phrase(phraseWords, size, languageWords)
  }

  // checksum section of phrase should be equal to hash of the entropy section
  private[mnemonic] def calculateChecksum(
    entropyBinaryString: String,
    size:                MnemonicSize
  ): String =
    byteTo8BitString(
      sha256
        .hash(
          // get the first `entropyLength` number of bits and hash the resulting byte array
          entropyBinaryString
            .slice(0, size.entropyLength)
            .grouped(byteLen)
            .toArray
            .map(Integer.parseInt(_, 2).toByte)
        )
        .value
        .head
    ).slice(0, size.checksumLength)

  private[mnemonic] def sizeFromNumberOfWords(numberOfWords: Int): Either[PhraseFailure, MnemonicSize] =
    numberOfWords match {
      case 12 => Right(MnemonicSizes.words12)
      case 15 => Right(MnemonicSizes.words15)
      case 18 => Right(MnemonicSizes.words18)
      case 21 => Right(MnemonicSizes.words21)
      case 24 => Right(MnemonicSizes.words24)
      case _  => Left(PhraseFailures.InvalidWordLength)
    }

}

sealed trait PhraseFailure

object PhraseFailures {
  case object InvalidWordLength extends PhraseFailure
  case object InvalidWords extends PhraseFailure
  case object InvalidChecksum extends PhraseFailure
  case object InvalidEntropyLength extends PhraseFailure
  case class WordListFailure(failure: LanguageWordList.ValidationFailure) extends PhraseFailure
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy