├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ ├── com │ │ └── sparrowwallet │ │ │ └── drongo │ │ │ ├── ApplicationAppender.java │ │ │ ├── BitcoinUnit.java │ │ │ ├── Drongo.java │ │ │ ├── ExtendedKey.java │ │ │ ├── FileType.java │ │ │ ├── IOUtils.java │ │ │ ├── KeyDerivation.java │ │ │ ├── KeyPurpose.java │ │ │ ├── LogHandler.java │ │ │ ├── NativeUtils.java │ │ │ ├── Network.java │ │ │ ├── OsType.java │ │ │ ├── OutputDescriptor.java │ │ │ ├── PropertyDefiner.java │ │ │ ├── SecureString.java │ │ │ ├── Utils.java │ │ │ ├── Version.java │ │ │ ├── WatchWallet.java │ │ │ ├── address │ │ │ ├── Address.java │ │ │ ├── InvalidAddressException.java │ │ │ ├── P2AAddress.java │ │ │ ├── P2PKAddress.java │ │ │ ├── P2PKHAddress.java │ │ │ ├── P2SHAddress.java │ │ │ ├── P2TRAddress.java │ │ │ ├── P2WPKHAddress.java │ │ │ └── P2WSHAddress.java │ │ │ ├── bip47 │ │ │ ├── InvalidPaymentCodeException.java │ │ │ ├── NotSecp256k1Exception.java │ │ │ ├── PaymentAddress.java │ │ │ ├── PaymentCode.java │ │ │ └── SecretPoint.java │ │ │ ├── crypto │ │ │ ├── AESKeyCrypter.java │ │ │ ├── Argon2KeyDeriver.java │ │ │ ├── AsymmetricKeyCrypter.java │ │ │ ├── AsymmetricKeyDeriver.java │ │ │ ├── BIP38.java │ │ │ ├── Bip322.java │ │ │ ├── ChildNumber.java │ │ │ ├── DeterministicHierarchy.java │ │ │ ├── DeterministicKey.java │ │ │ ├── DoubleSha256KeyDeriver.java │ │ │ ├── DumpedPrivateKey.java │ │ │ ├── ECDSASignature.java │ │ │ ├── ECIESKeyCrypter.java │ │ │ ├── ECKey.java │ │ │ ├── EncryptableItem.java │ │ │ ├── EncryptedData.java │ │ │ ├── EncryptionType.java │ │ │ ├── HDDerivationException.java │ │ │ ├── HDKeyDerivation.java │ │ │ ├── HMacDSANonceKCalculator.java │ │ │ ├── InvalidPasswordException.java │ │ │ ├── Key.java │ │ │ ├── KeyCrypter.java │ │ │ ├── KeyCrypterException.java │ │ │ ├── KeyDeriver.java │ │ │ ├── LazyECPoint.java │ │ │ ├── Pbkdf2KeyDeriver.java │ │ │ ├── SamouraiUtil.java │ │ │ ├── SchnorrSignature.java │ │ │ ├── ScryptKeyDeriver.java │ │ │ ├── Secp256r1Key.java │ │ │ ├── VersionedChecksummedBytes.java │ │ │ └── X25519Key.java │ │ │ ├── pgp │ │ │ ├── PGPKeySource.java │ │ │ ├── PGPUtils.java │ │ │ ├── PGPVerificationException.java │ │ │ └── PGPVerificationResult.java │ │ │ ├── policy │ │ │ ├── Miniscript.java │ │ │ ├── Policy.java │ │ │ ├── PolicyException.java │ │ │ └── PolicyType.java │ │ │ ├── protocol │ │ │ ├── Base43.java │ │ │ ├── Base58.java │ │ │ ├── Bech32.java │ │ │ ├── BlockHeader.java │ │ │ ├── ChildMessage.java │ │ │ ├── HashIndex.java │ │ │ ├── Message.java │ │ │ ├── NonStandardScriptException.java │ │ │ ├── ProtocolException.java │ │ │ ├── Ripemd160.java │ │ │ ├── Script.java │ │ │ ├── ScriptChunk.java │ │ │ ├── ScriptOpCodes.java │ │ │ ├── ScriptType.java │ │ │ ├── Sha256Hash.java │ │ │ ├── SigHash.java │ │ │ ├── SignatureDecodeException.java │ │ │ ├── Transaction.java │ │ │ ├── TransactionInput.java │ │ │ ├── TransactionOutPoint.java │ │ │ ├── TransactionOutput.java │ │ │ ├── TransactionSignature.java │ │ │ ├── TransactionWitness.java │ │ │ ├── UnsafeByteArrayOutputStream.java │ │ │ ├── VarInt.java │ │ │ └── VerificationException.java │ │ │ ├── psbt │ │ │ ├── PSBT.java │ │ │ ├── PSBTEntry.java │ │ │ ├── PSBTInput.java │ │ │ ├── PSBTInputSigner.java │ │ │ ├── PSBTOutput.java │ │ │ ├── PSBTParseException.java │ │ │ └── PSBTSignatureException.java │ │ │ ├── uri │ │ │ ├── BitcoinURI.java │ │ │ ├── BitcoinURIParseException.java │ │ │ ├── OptionalFieldValidationException.java │ │ │ └── RequiredFieldValidationException.java │ │ │ └── wallet │ │ │ ├── Bip39MnemonicCode.java │ │ │ ├── BlockTransaction.java │ │ │ ├── BlockTransactionHash.java │ │ │ ├── BlockTransactionHashIndex.java │ │ │ ├── BnBUtxoSelector.java │ │ │ ├── CoinbaseTxoFilter.java │ │ │ ├── DeterministicSeed.java │ │ │ ├── ElectrumMnemonicCode.java │ │ │ ├── ExcludeTxoFilter.java │ │ │ ├── FinalizingPSBTWallet.java │ │ │ ├── FrozenTxoFilter.java │ │ │ ├── InsufficientFundsException.java │ │ │ ├── InvalidKeystoreException.java │ │ │ ├── InvalidWalletException.java │ │ │ ├── Keystore.java │ │ │ ├── KeystoreSource.java │ │ │ ├── KnapsackUtxoSelector.java │ │ │ ├── MasterPrivateExtendedKey.java │ │ │ ├── MaxUtxoSelector.java │ │ │ ├── MixConfig.java │ │ │ ├── MnemonicException.java │ │ │ ├── OutputGroup.java │ │ │ ├── Payment.java │ │ │ ├── Persistable.java │ │ │ ├── PresetUtxoSelector.java │ │ │ ├── PriorityUtxoSelector.java │ │ │ ├── SeedQR.java │ │ │ ├── SingleSetUtxoSelector.java │ │ │ ├── SortDirection.java │ │ │ ├── SpentTxoFilter.java │ │ │ ├── StandardAccount.java │ │ │ ├── Status.java │ │ │ ├── StonewallUtxoSelector.java │ │ │ ├── TableType.java │ │ │ ├── TxoFilter.java │ │ │ ├── UtxoMixData.java │ │ │ ├── UtxoSelector.java │ │ │ ├── Wallet.java │ │ │ ├── WalletConfig.java │ │ │ ├── WalletModel.java │ │ │ ├── WalletNode.java │ │ │ ├── WalletTable.java │ │ │ ├── WalletTransaction.java │ │ │ └── slip39 │ │ │ ├── Cipher.java │ │ │ ├── EncryptedMasterSecret.java │ │ │ ├── GroupParams.java │ │ │ ├── RawShare.java │ │ │ ├── RecoveryState.java │ │ │ ├── Rs1024.java │ │ │ ├── Shamir.java │ │ │ ├── Share.java │ │ │ ├── ShareGroup.java │ │ │ ├── Slip39MnemonicCode.java │ │ │ └── Utils.java │ ├── module-info.java │ └── org │ │ └── bitcoin │ │ ├── NativeSecp256k1.java │ │ ├── NativeSecp256k1Util.java │ │ └── Secp256k1Context.java └── resources │ ├── gpg │ ├── Calin_Culianu_(NilacTheGrim)_calin.culianu_gmail.com.asc │ ├── Christian_Decker_decker.christian_gmail.com.asc │ ├── Craig_Raw_craig_sparrowwallet.com.asc │ ├── Ken_Carpenter_(CTO__Foundation_Devices__Inc.)_ken_foundationdevices.com.asc │ ├── Marko_Bencun_marko_shiftcrypto.ch.asc │ ├── Michael_Ford_(bitcoin_otc)_fanquake_gmail.com.asc │ ├── Olaoluwa_Osuntokun_laolu32_gmail.com.asc │ ├── Oliver_Gugger_gugger_gmail.com.asc │ ├── Peter_D._Gray_peter_coinkite.com.asc │ ├── SatoshiLabs_2021_Signing_Key.asc │ ├── ShiftCrypto_Security_security_shiftcrypto.ch.asc │ ├── Specter_Signer_noreply_specter.solutions.asc │ ├── Stepan_Snigirev_(Specter_release_signing_key)_snigirev.stepan_gmail.com.asc │ ├── T_Dev_D_(Samourai)_dev_samouraiwallet.com.asc │ ├── Thomas_Voegtlin_(https_x3a__electrum.org)_thomasv_electrum.org.asc │ └── seedsigner_btc.hardware.solutions_gmail.com.asc │ ├── native │ ├── linux │ │ ├── aarch64 │ │ │ └── libsecp256k1.so │ │ └── x64 │ │ │ └── libsecp256k1.so │ ├── osx │ │ ├── aarch64 │ │ │ └── libsecp256k1.dylib │ │ └── x64 │ │ │ └── libsecp256k1.dylib │ └── windows │ │ └── x64 │ │ └── libsecp256k1-0.dll │ └── wordlist │ ├── bip39-english.txt │ └── slip39.txt └── test └── java └── com └── sparrowwallet └── drongo ├── OutputDescriptorTest.java ├── WatchWalletTest.java ├── address └── AddressTest.java ├── bip47 └── PaymentCodeTest.java ├── crypto ├── Argon2KeyDeriverTest.java ├── BIP38Test.java ├── Bip322Test.java ├── ECIESKeyCrypterTest.java ├── ECKeyTest.java └── ScryptKeyDeriverTest.java ├── protocol └── TransactionTest.java ├── psbt └── PSBTTest.java ├── uri └── BitcoinUriTest.java └── wallet ├── Bip39MnemonicCodeTest.java ├── DeterministicSeedTest.java ├── KeystoreTest.java ├── PolicyTest.java ├── WalletTest.java └── slip39 └── ShareTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | *iml 4 | build 5 | /*.properties 6 | out 7 | *.log 8 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drongo 2 | 3 | Drongo is a Java Bitcoin library built mainly to support [Sparrow Wallet](https://sparrowwallet.com). 4 | 5 | ## Building 6 | 7 | Drongo can be built with 8 | 9 | `./gradlew jar` 10 | 11 | ## License 12 | 13 | Drongo is licensed under the Apache 2 software licence. 14 | 15 | ## Credits 16 | 17 | Drongo was inspired by (and is in part derived from) the [bitcoinj](https://bitcoinj.org) Bitcoin library. 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | tasks.withType(AbstractArchiveTask) { 6 | preserveFileTimestamps = false 7 | reproducibleFileOrder = true 8 | } 9 | 10 | def os = org.gradle.internal.os.OperatingSystem.current() 11 | def osName = os.getFamilyName() 12 | if(os.macOsX) { 13 | osName = "osx" 14 | } 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | dependencies { 21 | implementation ('org.bouncycastle:bcprov-jdk18on:1.80') 22 | implementation('org.pgpainless:pgpainless-core:1.7.5') { 23 | exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common' 24 | } 25 | implementation ('de.mkammerer:argon2-jvm:2.11') { 26 | exclude group: 'net.java.dev.jna', module: 'jna' 27 | } 28 | implementation ('net.java.dev.jna:jna:5.13.0') 29 | implementation ('ch.qos.logback:logback-classic:1.5.18') { 30 | exclude group: 'org.slf4j' 31 | } 32 | implementation ('org.slf4j:slf4j-api:2.0.12') 33 | testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0') 34 | testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0') 35 | testRuntimeOnly('org.junit.platform:junit-platform-launcher') 36 | } 37 | 38 | test { 39 | useJUnitPlatform() 40 | } 41 | 42 | processResources { 43 | doLast { 44 | delete fileTree("$buildDir/resources/main/native").matching { 45 | exclude "${osName}/**" 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparrowwallet/drongo/13e1fafbe8892d7005a043ae561e09ed66f7cea6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'drongo' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/ApplicationAppender.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.AppenderBase; 5 | import org.slf4j.event.Level; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | 9 | public class ApplicationAppender extends AppenderBase { 10 | private LogHandler callback; 11 | 12 | @Override 13 | protected void append(ILoggingEvent e) { 14 | callback.handleLog(e.getThreadName(), Level.valueOf(e.getLevel().toString()), e.getMessage(), e.getLoggerName(), e.getTimeStamp(), e.getCallerData()); 15 | } 16 | 17 | public void setCallback(String callback) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 18 | this.callback = (LogHandler)Class.forName(callback).getConstructor().newInstance(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/BitcoinUnit.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | import com.sparrowwallet.drongo.protocol.Transaction; 4 | 5 | public enum BitcoinUnit { 6 | AUTO("Auto") { 7 | @Override 8 | public long getSatsValue(double unitValue) { 9 | throw new UnsupportedOperationException("Auto unit cannot convert bitcoin values"); 10 | } 11 | 12 | @Override 13 | public double getValue(long satsValue) { 14 | throw new UnsupportedOperationException("Auto unit cannot convert bitcoin values"); 15 | } 16 | }, 17 | BTC("BTC") { 18 | @Override 19 | public long getSatsValue(double unitValue) { 20 | return Math.round(unitValue * Transaction.SATOSHIS_PER_BITCOIN); 21 | } 22 | 23 | @Override 24 | public double getValue(long satsValue) { 25 | return (double)satsValue / Transaction.SATOSHIS_PER_BITCOIN; 26 | } 27 | }, 28 | SATOSHIS("sats") { 29 | @Override 30 | public long getSatsValue(double unitValue) { 31 | return (long)unitValue; 32 | } 33 | 34 | @Override 35 | public double getValue(long satsValue) { 36 | return (double)satsValue; 37 | } 38 | }; 39 | 40 | private final String label; 41 | 42 | BitcoinUnit(String label) { 43 | this.label = label; 44 | } 45 | 46 | public String getLabel() { 47 | return label; 48 | } 49 | 50 | public abstract long getSatsValue(double unitValue); 51 | 52 | public abstract double getValue(long satsValue); 53 | 54 | public double convertFrom(double fromValue, BitcoinUnit fromUnit) { 55 | long satsValue = fromUnit.getSatsValue(fromValue); 56 | return getValue(satsValue); 57 | } 58 | 59 | public static long getAutoThreshold() { 60 | return 100000000L; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return label; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/Drongo.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 4 | import org.slf4j.LoggerFactory; 5 | import org.slf4j.event.Level; 6 | 7 | import java.security.Provider; 8 | 9 | public class Drongo { 10 | public static void setRootLogLevel(Level level) { 11 | ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); 12 | root.setLevel(ch.qos.logback.classic.Level.toLevel(level.toString())); 13 | } 14 | 15 | public static void removeRootLogAppender(String appenderName) { 16 | ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); 17 | root.detachAppender(appenderName); 18 | } 19 | 20 | public static Provider getProvider() { 21 | return new BouncyCastleProvider(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/FileType.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | public enum FileType { 4 | TEXT, JSON, BINARY, UNKNOWN; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/KeyPurpose.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | import com.sparrowwallet.drongo.crypto.ChildNumber; 4 | 5 | import java.util.List; 6 | 7 | public enum KeyPurpose { 8 | RECEIVE(ChildNumber.ZERO), CHANGE(ChildNumber.ONE); 9 | 10 | public static final List DEFAULT_PURPOSES = List.of(RECEIVE, CHANGE); 11 | 12 | //The receive derivation is also used for BIP47 notifications 13 | public static final KeyPurpose NOTIFICATION = RECEIVE; 14 | //The change derivation is reused for the send chain in BIP47 wallets 15 | public static final KeyPurpose SEND = CHANGE; 16 | 17 | private final ChildNumber pathIndex; 18 | 19 | KeyPurpose(ChildNumber pathIndex) { 20 | this.pathIndex = pathIndex; 21 | } 22 | 23 | public ChildNumber getPathIndex() { 24 | return pathIndex; 25 | } 26 | 27 | public static KeyPurpose fromChildNumber(ChildNumber childNumber) { 28 | for(KeyPurpose keyPurpose : values()) { 29 | if(keyPurpose.getPathIndex().equals(childNumber)) { 30 | return keyPurpose; 31 | } 32 | } 33 | 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/LogHandler.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | import org.slf4j.event.Level; 4 | 5 | public interface LogHandler { 6 | void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/OsType.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | public enum OsType { 4 | WINDOWS("Windows"), 5 | MACOS("macOS"), 6 | UNIX("Unix"), 7 | UNKNOWN(""); 8 | 9 | private static final OsType current = getCurrentPlatform(); 10 | private final String platformId; 11 | 12 | OsType(String platformId) { 13 | this.platformId = platformId; 14 | } 15 | 16 | /** 17 | * Returns platform id. Usually used to specify platform dependent styles 18 | * 19 | * @return platform id 20 | */ 21 | public String getPlatformId() { 22 | return platformId; 23 | } 24 | 25 | /** 26 | * @return the current OS. 27 | */ 28 | public static OsType getCurrent() { 29 | return current; 30 | } 31 | 32 | private static OsType getCurrentPlatform() { 33 | String osName = System.getProperty("os.name"); 34 | if(osName.startsWith("Windows")) { 35 | return WINDOWS; 36 | } 37 | if(osName.startsWith("Mac")) { 38 | return MACOS; 39 | } 40 | if(osName.startsWith("SunOS")) { 41 | return UNIX; 42 | } 43 | if(osName.startsWith("Linux")) { 44 | return UNIX; 45 | } 46 | return UNKNOWN; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/PropertyDefiner.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | import ch.qos.logback.core.PropertyDefinerBase; 4 | 5 | import java.util.Locale; 6 | 7 | public class PropertyDefiner extends PropertyDefinerBase { 8 | private String application; 9 | 10 | public void setApplication(String application) { 11 | this.application = application; 12 | } 13 | 14 | @Override 15 | public String getPropertyValue() { 16 | if(System.getProperty(application.toLowerCase(Locale.ROOT) + ".home") != null) { 17 | return System.getProperty(application.toLowerCase(Locale.ROOT) + ".home"); 18 | } 19 | 20 | return isWindows() ? System.getenv("APPDATA") + "/" + application.substring(0, 1).toUpperCase(Locale.ROOT) + application.substring(1).toLowerCase(Locale.ROOT) : System.getProperty("user.home") + "/." + application.toLowerCase(Locale.ROOT); 21 | } 22 | 23 | private boolean isWindows() { 24 | String osName = System.getProperty("os.name"); 25 | return (osName != null && osName.toLowerCase(Locale.ROOT).startsWith("windows")); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/Version.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | public class Version implements Comparable { 4 | private final String version; 5 | 6 | public final String get() { 7 | return this.version; 8 | } 9 | 10 | public Version(String version) { 11 | if(version == null) { 12 | throw new IllegalArgumentException("Version can not be null"); 13 | } 14 | version = version.replaceAll("[^0-9.].*", ""); 15 | if(!version.matches("[0-9]+(\\.[0-9]+)*")) { 16 | throw new IllegalArgumentException("Invalid version format"); 17 | } 18 | this.version = version; 19 | } 20 | 21 | @Override 22 | public int compareTo(Version that) { 23 | if(that == null) { 24 | return 1; 25 | } 26 | String[] thisParts = this.get().split("\\."); 27 | String[] thatParts = that.get().split("\\."); 28 | int length = Math.max(thisParts.length, thatParts.length); 29 | for(int i = 0; i < length; i++) { 30 | int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0; 31 | int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0; 32 | if(thisPart < thatPart) { 33 | return -1; 34 | } 35 | if(thisPart > thatPart) { 36 | return 1; 37 | } 38 | } 39 | return 0; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object that) { 44 | if(this == that) { 45 | return true; 46 | } 47 | if(that == null) { 48 | return false; 49 | } 50 | if(this.getClass() != that.getClass()) { 51 | return false; 52 | } 53 | return this.compareTo((Version) that) == 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/WatchWallet.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo; 2 | 3 | import com.sparrowwallet.drongo.address.Address; 4 | import com.sparrowwallet.drongo.crypto.*; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | 9 | public class WatchWallet { 10 | private static final int LOOK_AHEAD_LIMIT = 500; 11 | 12 | private String name; 13 | private OutputDescriptor outputDescriptor; 14 | 15 | private HashMap> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2); 16 | 17 | public WatchWallet(String name, String descriptor) { 18 | this.name = name; 19 | this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor); 20 | } 21 | 22 | public void initialiseAddresses() { 23 | if(outputDescriptor.describesMultipleAddresses()) { 24 | for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { 25 | List receivingDerivation = outputDescriptor.getReceivingDerivation(index); 26 | Address address = getReceivingAddress(index); 27 | addresses.put(address, receivingDerivation); 28 | } 29 | 30 | for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { 31 | List changeDerivation = outputDescriptor.getChangeDerivation(index); 32 | Address address = getChangeAddress(index); 33 | addresses.put(address, changeDerivation); 34 | } 35 | } else { 36 | List derivation = outputDescriptor.getChildDerivation(); 37 | Address address = outputDescriptor.getAddress(derivation); 38 | addresses.put(address, derivation); 39 | } 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public boolean containsAddress(Address address) { 47 | return addresses.containsKey(address); 48 | } 49 | 50 | public List getAddressPath(Address address) { 51 | return addresses.get(address); 52 | } 53 | 54 | public Address getReceivingAddress(int index) { 55 | return getAddress(outputDescriptor.getReceivingDerivation(index)); 56 | } 57 | 58 | public Address getChangeAddress(int index) { 59 | return getAddress(outputDescriptor.getChangeDerivation(index)); 60 | } 61 | 62 | public OutputDescriptor getOutputDescriptor() { 63 | return outputDescriptor; 64 | } 65 | 66 | public Address getAddress(List path) { 67 | return outputDescriptor.getAddress(path); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/InvalidAddressException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | public class InvalidAddressException extends Exception { 4 | public InvalidAddressException() { 5 | super(); 6 | } 7 | 8 | public InvalidAddressException(String msg) { 9 | super(msg); 10 | } 11 | 12 | public InvalidAddressException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public InvalidAddressException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/P2AAddress.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | import com.sparrowwallet.drongo.Network; 4 | import com.sparrowwallet.drongo.protocol.Bech32; 5 | import com.sparrowwallet.drongo.protocol.ScriptType; 6 | 7 | public class P2AAddress extends Address { 8 | public P2AAddress(byte[] data) { 9 | super(data); 10 | } 11 | 12 | @Override 13 | public int getVersion(Network network) { 14 | return 1; 15 | } 16 | 17 | @Override 18 | public String getAddress(Network network) { 19 | return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data); 20 | } 21 | 22 | @Override 23 | public ScriptType getScriptType() { 24 | return ScriptType.P2A; 25 | } 26 | 27 | @Override 28 | public String getOutputScriptDataType() { 29 | return "Anchor"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | import com.sparrowwallet.drongo.Network; 4 | import com.sparrowwallet.drongo.Utils; 5 | import com.sparrowwallet.drongo.protocol.Script; 6 | import com.sparrowwallet.drongo.protocol.ScriptType; 7 | 8 | public class P2PKAddress extends Address { 9 | public P2PKAddress(byte[] pubKey) { 10 | super(pubKey); 11 | } 12 | 13 | @Override 14 | public int getVersion(Network network) { 15 | return network.getP2PKHAddressHeader(); 16 | } 17 | 18 | @Override 19 | public String getAddress(Network network) { 20 | return Utils.bytesToHex(data); 21 | } 22 | 23 | public ScriptType getScriptType() { 24 | return ScriptType.P2PK; 25 | } 26 | 27 | @Override 28 | public String getOutputScriptDataType() { 29 | return "Public Key"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/P2PKHAddress.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | import com.sparrowwallet.drongo.Network; 4 | import com.sparrowwallet.drongo.protocol.Script; 5 | import com.sparrowwallet.drongo.protocol.ScriptType; 6 | 7 | public class P2PKHAddress extends Address { 8 | public P2PKHAddress(byte[] pubKeyHash) { 9 | super(pubKeyHash); 10 | } 11 | 12 | @Override 13 | public int getVersion(Network network) { 14 | return network.getP2PKHAddressHeader(); 15 | } 16 | 17 | @Override 18 | public ScriptType getScriptType() { 19 | return ScriptType.P2PKH; 20 | } 21 | 22 | @Override 23 | public String getOutputScriptDataType() { 24 | return "Public Key Hash"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/P2SHAddress.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | import com.sparrowwallet.drongo.Network; 4 | import com.sparrowwallet.drongo.Utils; 5 | import com.sparrowwallet.drongo.protocol.Script; 6 | import com.sparrowwallet.drongo.protocol.ScriptType; 7 | 8 | public class P2SHAddress extends Address { 9 | public P2SHAddress(byte[] scriptHash) { 10 | super(scriptHash); 11 | } 12 | 13 | @Override 14 | public int getVersion(Network network) { 15 | return network.getP2SHAddressHeader(); 16 | } 17 | 18 | @Override 19 | public ScriptType getScriptType() { 20 | return ScriptType.P2SH; 21 | } 22 | 23 | @Override 24 | public String getOutputScriptDataType() { 25 | return "Script Hash"; 26 | } 27 | 28 | public static P2SHAddress fromProgram(byte[] program) { 29 | return new P2SHAddress(Utils.sha256hash160(program)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/P2TRAddress.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | import com.sparrowwallet.drongo.Network; 4 | import com.sparrowwallet.drongo.protocol.Bech32; 5 | import com.sparrowwallet.drongo.protocol.Script; 6 | import com.sparrowwallet.drongo.protocol.ScriptType; 7 | 8 | public class P2TRAddress extends Address { 9 | public P2TRAddress(byte[] pubKey) { 10 | super(pubKey); 11 | } 12 | 13 | @Override 14 | public int getVersion(Network network) { 15 | return 1; 16 | } 17 | 18 | @Override 19 | public String getAddress(Network network) { 20 | return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data); 21 | } 22 | 23 | @Override 24 | public ScriptType getScriptType() { 25 | return ScriptType.P2TR; 26 | } 27 | 28 | @Override 29 | public String getOutputScriptDataType() { 30 | return "Taproot"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/P2WPKHAddress.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | import com.sparrowwallet.drongo.Network; 4 | import com.sparrowwallet.drongo.protocol.Bech32; 5 | import com.sparrowwallet.drongo.protocol.Script; 6 | import com.sparrowwallet.drongo.protocol.ScriptType; 7 | 8 | public class P2WPKHAddress extends Address { 9 | public P2WPKHAddress(byte[] pubKeyHash) { 10 | super(pubKeyHash); 11 | } 12 | 13 | @Override 14 | public int getVersion(Network network) { 15 | return 0; 16 | } 17 | 18 | @Override 19 | public String getAddress(Network network) { 20 | return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data); 21 | } 22 | 23 | @Override 24 | public ScriptType getScriptType() { 25 | return ScriptType.P2WPKH; 26 | } 27 | 28 | @Override 29 | public String getOutputScriptDataType() { 30 | return "Witness Public Key Hash"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/address/P2WSHAddress.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.address; 2 | 3 | import com.sparrowwallet.drongo.Network; 4 | import com.sparrowwallet.drongo.protocol.*; 5 | 6 | public class P2WSHAddress extends Address { 7 | public P2WSHAddress(byte[] scriptHash) { 8 | super(scriptHash); 9 | } 10 | 11 | @Override 12 | public int getVersion(Network network) { 13 | return 0; 14 | } 15 | 16 | @Override 17 | public String getAddress(Network network) { 18 | return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data); 19 | } 20 | 21 | @Override 22 | public ScriptType getScriptType() { 23 | return ScriptType.P2WSH; 24 | } 25 | 26 | @Override 27 | public String getOutputScriptDataType() { 28 | return "Witness Script Hash"; 29 | } 30 | 31 | public static P2WSHAddress fromProgram(byte[] program) { 32 | return new P2WSHAddress(Sha256Hash.hash(program)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/bip47/InvalidPaymentCodeException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.bip47; 2 | 3 | public class InvalidPaymentCodeException extends Exception { 4 | public InvalidPaymentCodeException() { 5 | super(); 6 | } 7 | 8 | public InvalidPaymentCodeException(String msg) { 9 | super(msg); 10 | } 11 | 12 | public InvalidPaymentCodeException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public InvalidPaymentCodeException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/bip47/NotSecp256k1Exception.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.bip47; 2 | 3 | public class NotSecp256k1Exception extends Exception { 4 | public NotSecp256k1Exception() { 5 | super(); 6 | } 7 | 8 | public NotSecp256k1Exception(String msg) { 9 | super(msg); 10 | } 11 | 12 | public NotSecp256k1Exception(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public NotSecp256k1Exception(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/bip47/SecretPoint.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.bip47; 2 | 3 | import org.bouncycastle.jce.ECNamedCurveTable; 4 | import org.bouncycastle.jce.spec.ECParameterSpec; 5 | import org.bouncycastle.jce.spec.ECPrivateKeySpec; 6 | import org.bouncycastle.jce.spec.ECPublicKeySpec; 7 | 8 | import javax.crypto.KeyAgreement; 9 | import javax.crypto.SecretKey; 10 | import java.math.BigInteger; 11 | import java.security.*; 12 | import java.security.spec.InvalidKeySpecException; 13 | 14 | public class SecretPoint { 15 | private static final ECParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1"); 16 | private static final String KEY_PROVIDER = "BC"; 17 | 18 | private final PrivateKey privKey; 19 | private final PublicKey pubKey; 20 | private final KeyFactory kf; 21 | 22 | static { 23 | Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); 24 | } 25 | 26 | public SecretPoint(byte[] dataPrv, byte[] dataPub) throws InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { 27 | kf = KeyFactory.getInstance("ECDH", KEY_PROVIDER); 28 | privKey = loadPrivateKey(dataPrv); 29 | pubKey = loadPublicKey(dataPub); 30 | } 31 | 32 | public byte[] ECDHSecretAsBytes() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { 33 | return ECDHSecret().getEncoded(); 34 | } 35 | 36 | private SecretKey ECDHSecret() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { 37 | KeyAgreement ka = KeyAgreement.getInstance("ECDH", KEY_PROVIDER); 38 | ka.init(privKey); 39 | ka.doPhase(pubKey, true); 40 | return ka.generateSecret("AES"); 41 | } 42 | 43 | private PublicKey loadPublicKey(byte[] data) throws InvalidKeySpecException { 44 | ECPublicKeySpec pubKey = new ECPublicKeySpec(params.getCurve().decodePoint(data), params); 45 | return kf.generatePublic(pubKey); 46 | } 47 | 48 | private PrivateKey loadPrivateKey(byte[] data) throws InvalidKeySpecException { 49 | ECPrivateKeySpec prvkey = new ECPrivateKeySpec(new BigInteger(1, data), params); 50 | return kf.generatePrivate(prvkey); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/AESKeyCrypter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import org.bouncycastle.crypto.BufferedBlockCipher; 4 | import org.bouncycastle.crypto.InvalidCipherTextException; 5 | import org.bouncycastle.crypto.engines.AESEngine; 6 | import org.bouncycastle.crypto.modes.CBCBlockCipher; 7 | import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; 8 | import org.bouncycastle.crypto.params.KeyParameter; 9 | import org.bouncycastle.crypto.params.ParametersWithIV; 10 | 11 | import java.security.SecureRandom; 12 | import java.util.Arrays; 13 | 14 | /* 15 | * Performs AES/CBC/PKCS7 encryption and decryption 16 | */ 17 | public class AESKeyCrypter implements KeyCrypter { 18 | /** 19 | * The size of an AES block in bytes. 20 | * This is also the length of the initialisation vector. 21 | */ 22 | public static final int BLOCK_LENGTH = 16; // = 128 bits. 23 | 24 | private static final SecureRandom secureRandom = new SecureRandom(); 25 | 26 | /** 27 | * Decrypt bytes previously encrypted with this class. 28 | * 29 | * @param dataToDecrypt The data to decrypt 30 | * @param aesKey The AES key to use for decryption 31 | * @return The decrypted bytes 32 | * @throws KeyCrypterException if bytes could not be decrypted 33 | */ 34 | @Override 35 | public byte[] decrypt(EncryptedData dataToDecrypt, Key aesKey) throws KeyCrypterException { 36 | if(dataToDecrypt == null || aesKey == null) { 37 | throw new KeyCrypterException("Data and key to decrypt cannot be null"); 38 | } 39 | 40 | try { 41 | ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKeyBytes()), dataToDecrypt.getInitialisationVector()); 42 | 43 | // Decrypt the message. 44 | BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(CBCBlockCipher.newInstance(AESEngine.newInstance())); 45 | cipher.init(false, keyWithIv); 46 | 47 | byte[] cipherBytes = dataToDecrypt.getEncryptedBytes(); 48 | byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)]; 49 | final int length1 = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0); 50 | final int length2 = cipher.doFinal(decryptedBytes, length1); 51 | 52 | byte[] decrypted = Arrays.copyOf(decryptedBytes, length1 + length2); 53 | Arrays.fill(decryptedBytes, (byte)0); 54 | return decrypted; 55 | } catch (InvalidCipherTextException e) { 56 | throw new KeyCrypterException.InvalidCipherText("Could not decrypt bytes", e); 57 | } catch (RuntimeException e) { 58 | throw new KeyCrypterException("Could not decrypt bytes", e); 59 | } 60 | } 61 | 62 | /** 63 | * Password based encryption using AES - CBC - PKCS7 64 | */ 65 | @Override 66 | public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, Key aesKey) throws KeyCrypterException { 67 | if(plainBytes == null || aesKey == null) { 68 | throw new KeyCrypterException("Data and key to encrypt cannot be null"); 69 | } 70 | 71 | try { 72 | // Generate iv - each encryption call has a different iv. 73 | byte[] iv = initializationVector; 74 | if(iv == null) { 75 | iv = new byte[BLOCK_LENGTH]; 76 | secureRandom.nextBytes(iv); 77 | } 78 | 79 | ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKeyBytes()), iv); 80 | 81 | // Encrypt using AES. 82 | BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(CBCBlockCipher.newInstance(AESEngine.newInstance())); 83 | cipher.init(true, keyWithIv); 84 | byte[] encryptedBytes = new byte[cipher.getOutputSize(plainBytes.length)]; 85 | final int length1 = cipher.processBytes(plainBytes, 0, plainBytes.length, encryptedBytes, 0); 86 | final int length2 = cipher.doFinal(encryptedBytes, length1); 87 | 88 | return new EncryptedData(iv, Arrays.copyOf(encryptedBytes, length1 + length2), aesKey.getSalt(), aesKey.getDeriver(), getCrypterType()); 89 | } catch (Exception e) { 90 | throw new KeyCrypterException("Could not encrypt bytes.", e); 91 | } 92 | } 93 | 94 | @Override 95 | public EncryptionType.Crypter getCrypterType() { 96 | return EncryptionType.Crypter.AES_CBC_PKCS7; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/Argon2KeyDeriver.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import com.sparrowwallet.drongo.SecureString; 4 | import de.mkammerer.argon2.Argon2Advanced; 5 | import de.mkammerer.argon2.Argon2Factory; 6 | 7 | import java.security.SecureRandom; 8 | 9 | public class Argon2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver { 10 | public static final Argon2Parameters TEST_PARAMETERS = new Argon2Parameters(16, 32, 1, 1024, 1); 11 | public static final Argon2Parameters SPRW1_PARAMETERS = new Argon2Parameters(16, 32, 10, 256 * 1024, 4); 12 | 13 | private final Argon2Parameters argon2Parameters; 14 | private final byte[] salt; 15 | 16 | public Argon2KeyDeriver() { 17 | this(isTest() ? TEST_PARAMETERS : SPRW1_PARAMETERS); 18 | } 19 | 20 | public Argon2KeyDeriver(Argon2Parameters argon2Parameters) { 21 | this.argon2Parameters = argon2Parameters; 22 | 23 | SecureRandom secureRandom = new SecureRandom(); 24 | salt = new byte[argon2Parameters.saltLength]; 25 | secureRandom.nextBytes(salt); 26 | } 27 | 28 | public Argon2KeyDeriver(byte[] salt) { 29 | this(isTest() ? TEST_PARAMETERS : SPRW1_PARAMETERS, salt); 30 | } 31 | 32 | public Argon2KeyDeriver(Argon2Parameters argon2Parameters, byte[] salt) { 33 | this.argon2Parameters = argon2Parameters; 34 | this.salt = salt; 35 | } 36 | 37 | @Override 38 | public byte[] getSalt() { 39 | return salt; 40 | } 41 | 42 | @Override 43 | public Key deriveKey(CharSequence password) throws KeyCrypterException { 44 | Argon2Advanced argon2 = Argon2Factory.createAdvanced(Argon2Factory.Argon2Types.ARGON2id, argon2Parameters.saltLength, argon2Parameters.hashLength); 45 | byte[] hash = argon2.rawHash(argon2Parameters.iterations, argon2Parameters.memory, argon2Parameters.parallelism, SecureString.toBytesUTF8(password), salt); 46 | return new Key(hash, salt, getDeriverType()); 47 | } 48 | 49 | @Override 50 | public ECKey deriveECKey(CharSequence password) throws KeyCrypterException { 51 | Key key = deriveKey(password); 52 | return ECKey.fromPrivate(key.getKeyBytes()); 53 | } 54 | 55 | @Override 56 | public EncryptionType.Deriver getDeriverType() { 57 | return EncryptionType.Deriver.ARGON2; 58 | } 59 | 60 | private static boolean isTest() { 61 | return System.getProperty("org.gradle.test.worker") != null; 62 | } 63 | 64 | public static class Argon2Parameters { 65 | public final int saltLength; 66 | public final int hashLength; 67 | public final int iterations; 68 | public final int memory; 69 | public final int parallelism; 70 | 71 | public Argon2Parameters(int saltLength, int hashLength, int iterations, int memory, int parallelism) { 72 | this.saltLength = saltLength; 73 | this.hashLength = hashLength; 74 | this.iterations = iterations; 75 | this.memory = memory; 76 | this.parallelism = parallelism; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyCrypter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | public interface AsymmetricKeyCrypter { 4 | /** 5 | * Decrypt the provided encrypted bytes, converting them into unencrypted bytes. 6 | * 7 | * @throws KeyCrypterException if decryption was unsuccessful. 8 | */ 9 | byte[] decrypt(EncryptedData encryptedBytesToDecode, ECKey key) throws KeyCrypterException; 10 | 11 | /** 12 | * Encrypt the supplied bytes, converting them into ciphertext. 13 | * 14 | * @return encryptedPrivateKey An encryptedPrivateKey containing the encrypted bytes and an initialisation vector. 15 | * @throws KeyCrypterException if encryption was unsuccessful 16 | */ 17 | EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, ECKey key) throws KeyCrypterException; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyDeriver.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | public interface AsymmetricKeyDeriver { 4 | /** 5 | * Create a ECKey based on the provided password 6 | * @param password 7 | * @return ECKey The ECKey to use for encrypting and decrypting 8 | * @throws KeyCrypterException 9 | */ 10 | ECKey deriveECKey(CharSequence password) throws KeyCrypterException; 11 | 12 | byte[] getSalt(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/ChildNumber.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import java.util.Locale; 4 | 5 | public class ChildNumber { 6 | /** 7 | * The bit that's set in the child number to indicate whether this key is "hardened". Given a hardened key, it is 8 | * not possible to derive a child public key if you know only the hardened public key. With a non-hardened key this 9 | * is possible, so you can derive trees of public keys given only a public parent, but the downside is that it's 10 | * possible to leak private keys if you disclose a parent public key and a child private key (elliptic curve maths 11 | * allows you to work upwards). 12 | */ 13 | public static final int HARDENED_BIT = 0x80000000; 14 | 15 | public static final ChildNumber ZERO = new ChildNumber(0); 16 | public static final ChildNumber ZERO_HARDENED = new ChildNumber(0, true); 17 | public static final ChildNumber ONE = new ChildNumber(1); 18 | public static final ChildNumber ONE_HARDENED = new ChildNumber(1, true); 19 | 20 | /** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/ 21 | private final int i; 22 | 23 | public ChildNumber(int childNumber, boolean isHardened) { 24 | if (hasHardenedBit(childNumber)) 25 | throw new IllegalArgumentException("Most significant bit is reserved and shouldn't be set: " + childNumber); 26 | i = isHardened ? (childNumber | HARDENED_BIT) : childNumber; 27 | } 28 | 29 | public ChildNumber(int i) { 30 | this.i = i; 31 | } 32 | 33 | public static boolean hasHardenedBit(int a) { 34 | return (a & HARDENED_BIT) != 0; 35 | } 36 | 37 | public boolean isHardened() { 38 | return hasHardenedBit(i); 39 | } 40 | 41 | public int num() { 42 | return i & (~HARDENED_BIT); 43 | } 44 | 45 | /** Returns the uint32 encoded form of the path element, including the most significant bit. */ 46 | public int i() { return i; } 47 | 48 | public String toString() { 49 | return toString(true); 50 | } 51 | 52 | public String toString(boolean useApostrophes) { 53 | return num() + (isHardened() ? (useApostrophes ? "'" : "h") : ""); 54 | } 55 | 56 | public boolean equals(Object o) { 57 | if (this == o) return true; 58 | if (o == null || getClass() != o.getClass()) return false; 59 | return i == ((ChildNumber)o).i; 60 | } 61 | 62 | public int hashCode() { 63 | return i; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/DeterministicHierarchy.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | public class DeterministicHierarchy { 8 | private final Map, DeterministicKey> keys = new HashMap<>(); 9 | private final List rootPath; 10 | // Keep track of how many child keys each node has. This is kind of weak. 11 | private final Map, ChildNumber> lastChildNumbers = new HashMap<>(); 12 | 13 | public DeterministicHierarchy(DeterministicKey rootKey) { 14 | putKey(rootKey); 15 | rootPath = rootKey.getPath(); 16 | } 17 | 18 | public final void putKey(DeterministicKey key) { 19 | List path = key.getPath(); 20 | // Update our tracking of what the next child in each branch of the tree should be. Just assume that keys are 21 | // inserted in order here. 22 | final DeterministicKey parent = key.getParent(); 23 | if (parent != null) 24 | lastChildNumbers.put(parent.getPath(), key.getChildNumber()); 25 | keys.put(path, key); 26 | } 27 | 28 | /** 29 | * Returns a key for the given path, optionally creating it. 30 | * 31 | * @param path the path to the key 32 | * @return next newly created key using the child derivation function 33 | * @throws HDDerivationException if create is false and the path was not found. 34 | */ 35 | public DeterministicKey get(List path) throws HDDerivationException { 36 | if(!keys.containsKey(path)) { 37 | if(path.size() == 0) { 38 | throw new IllegalArgumentException("Can't derive the master key: nothing to derive from."); 39 | } 40 | 41 | DeterministicKey parent = get(path.subList(0, path.size() - 1)); 42 | putKey(HDKeyDerivation.deriveChildKey(parent, path.get(path.size() - 1))); 43 | } 44 | 45 | return keys.get(path); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/DoubleSha256KeyDeriver.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import com.sparrowwallet.drongo.SecureString; 4 | import com.sparrowwallet.drongo.protocol.Sha256Hash; 5 | 6 | public class DoubleSha256KeyDeriver implements KeyDeriver { 7 | 8 | @Override 9 | public Key deriveKey(CharSequence password) throws KeyCrypterException { 10 | byte[] passwordBytes = SecureString.toBytesUTF8(password); 11 | byte[] sha256 = Sha256Hash.hash(passwordBytes); 12 | byte[] doubleSha256 = Sha256Hash.hash(sha256); 13 | return new Key(doubleSha256, null, getDeriverType()); 14 | } 15 | 16 | @Override 17 | public EncryptionType.Deriver getDeriverType() { 18 | return EncryptionType.Deriver.DOUBLE_SHA256; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/DumpedPrivateKey.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | 4 | import com.sparrowwallet.drongo.Network; 5 | 6 | import java.util.Arrays; 7 | import java.util.Objects; 8 | 9 | /** 10 | * Parses and generates private keys in the form used by the Bitcoin "dumpprivkey" command. This is the private key 11 | * bytes with a header byte and 4 checksum bytes at the end. If there are 33 private key bytes instead of 32, then 12 | * the last byte is a discriminator value for the compressed pubkey. 13 | */ 14 | public class DumpedPrivateKey extends VersionedChecksummedBytes { 15 | private final boolean compressed; 16 | 17 | /** 18 | * Construct a private key from its Base58 representation. 19 | * @param base58 20 | * The textual form of the private key. 21 | */ 22 | public static DumpedPrivateKey fromBase58(String base58) { 23 | return new DumpedPrivateKey(base58); 24 | } 25 | 26 | // Used by ECKey.getPrivateKeyEncoded() 27 | DumpedPrivateKey(byte[] keyBytes, boolean compressed) { 28 | super(Network.get().getDumpedPrivateKeyHeader(), encode(keyBytes, compressed)); 29 | this.compressed = compressed; 30 | } 31 | 32 | private static byte[] encode(byte[] keyBytes, boolean compressed) { 33 | if(keyBytes.length != 32) { 34 | throw new IllegalArgumentException("Private keys must be 32 bytes"); 35 | } 36 | 37 | if (!compressed) { 38 | return keyBytes; 39 | } else { 40 | // Keys that have compressed public components have an extra 1 byte on the end in dumped form. 41 | byte[] bytes = new byte[33]; 42 | System.arraycopy(keyBytes, 0, bytes, 0, 32); 43 | bytes[32] = 1; 44 | return bytes; 45 | } 46 | } 47 | 48 | private DumpedPrivateKey(String encoded) { 49 | super(encoded); 50 | if(version != Network.get().getDumpedPrivateKeyHeader()) 51 | throw new IllegalArgumentException("Invalid version " + version + " for network " + Network.getCanonical()); 52 | if(bytes.length == 33 && bytes[32] == 1) { 53 | compressed = true; 54 | bytes = Arrays.copyOf(bytes, 32); // Chop off the additional marker byte. 55 | } else if(bytes.length == 32) { 56 | compressed = false; 57 | } else { 58 | throw new IllegalArgumentException("Wrong number of bytes for a private key, not 32 or 33"); 59 | } 60 | } 61 | 62 | /** 63 | * Returns an ECKey created from this encoded private key. 64 | */ 65 | public ECKey getKey() { 66 | return ECKey.fromPrivate(bytes, compressed); 67 | } 68 | 69 | @Override 70 | public boolean equals(Object o) { 71 | if(this == o) { 72 | return true; 73 | } 74 | if(o == null || getClass() != o.getClass()) { 75 | return false; 76 | } 77 | if(!super.equals(o)) { 78 | return false; 79 | } 80 | DumpedPrivateKey that = (DumpedPrivateKey) o; 81 | return compressed == that.compressed; 82 | } 83 | 84 | @Override 85 | public int hashCode() { 86 | return Objects.hash(super.hashCode(), compressed); 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/EncryptableItem.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | /** 4 | * Provides a uniform way to access something that can be optionally encrypted with a 5 | * {@link KeyCrypter}, yielding an {@link EncryptedData}, and 6 | * which can have a creation time associated with it. 7 | */ 8 | public interface EncryptableItem { 9 | /** Returns whether the item is encrypted or not. If it is, then {@link #getSecretBytes()} will return null. */ 10 | boolean isEncrypted(); 11 | 12 | /** Returns the raw bytes of the item, if not encrypted, or null if encrypted or the secret is missing. */ 13 | byte[] getSecretBytes(); 14 | 15 | /** Returns the initialization vector and encrypted secret bytes, or null if not encrypted. */ 16 | EncryptedData getEncryptedData(); 17 | 18 | /** Returns an object containing enums describing which algorithms are used to derive the key and encrypt the data. */ 19 | EncryptionType getEncryptionType(); 20 | 21 | /** Returns the time in seconds since the UNIX epoch at which this encryptable item was first created/derived. */ 22 | long getCreationTimeSeconds(); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/EncryptedData.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | 6 | /** 7 | *

An instance of EncryptedData is a holder for an initialization vector and encrypted bytes. It is typically 8 | * used to hold encrypted private key bytes.

9 | * 10 | *

The initialisation vector is random data that is used to initialise the AES block cipher when the 11 | * private key bytes were encrypted. You need these for decryption.

12 | */ 13 | public final class EncryptedData { 14 | private final byte[] initialisationVector; 15 | private final byte[] encryptedBytes; 16 | private final byte[] keySalt; 17 | private final EncryptionType encryptionType; 18 | 19 | public EncryptedData(byte[] initialisationVector, byte[] encryptedBytes, byte[] keySalt, EncryptionType.Deriver deriver, EncryptionType.Crypter crypter) { 20 | this(initialisationVector, encryptedBytes, keySalt, new EncryptionType(deriver, crypter)); 21 | } 22 | 23 | public EncryptedData(byte[] initialisationVector, byte[] encryptedBytes, byte[] keySalt, EncryptionType encryptionType) { 24 | this.initialisationVector = Arrays.copyOf(initialisationVector, initialisationVector.length); 25 | this.encryptedBytes = Arrays.copyOf(encryptedBytes, encryptedBytes.length); 26 | this.keySalt = keySalt == null ? null : Arrays.copyOf(keySalt, keySalt.length); 27 | this.encryptionType = encryptionType; 28 | } 29 | 30 | public byte[] getInitialisationVector() { 31 | return initialisationVector; 32 | } 33 | 34 | public byte[] getEncryptedBytes() { 35 | return encryptedBytes; 36 | } 37 | 38 | public byte[] getKeySalt() { 39 | return keySalt; 40 | } 41 | 42 | public EncryptionType getEncryptionType() { 43 | return encryptionType; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) return true; 49 | if (o == null || getClass() != o.getClass()) return false; 50 | EncryptedData other = (EncryptedData) o; 51 | return Arrays.equals(encryptedBytes, other.encryptedBytes) && 52 | Arrays.equals(initialisationVector, other.initialisationVector) && 53 | Arrays.equals(keySalt, other.keySalt) && 54 | encryptionType.equals(other.encryptionType); 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(Arrays.hashCode(encryptedBytes), Arrays.hashCode(initialisationVector), Arrays.hashCode(keySalt), encryptionType.hashCode()); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector) 65 | + ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) 66 | + ", keySalt=" + Arrays.toString(keySalt) 67 | + ", type=" + encryptionType + "]"; 68 | } 69 | 70 | public EncryptedData copy() { 71 | return new EncryptedData(Arrays.copyOf(initialisationVector, initialisationVector.length), 72 | Arrays.copyOf(encryptedBytes, encryptedBytes.length), 73 | Arrays.copyOf(keySalt, keySalt.length), 74 | encryptionType); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import com.sparrowwallet.drongo.SecureString; 4 | 5 | import java.util.Objects; 6 | 7 | public class EncryptionType { 8 | public enum Deriver { 9 | NONE() { 10 | public KeyDeriver getKeyDeriver() { 11 | return new KeyDeriver() { 12 | @Override 13 | public Key deriveKey(CharSequence password) throws KeyCrypterException { 14 | return new Key(SecureString.toBytesUTF8(password), null, NONE); 15 | } 16 | 17 | @Override 18 | public Deriver getDeriverType() { 19 | return NONE; 20 | } 21 | }; 22 | } 23 | }, 24 | DOUBLE_SHA256() { 25 | public KeyDeriver getKeyDeriver() { 26 | return new DoubleSha256KeyDeriver(); 27 | } 28 | }, 29 | PBKDF2() { 30 | public KeyDeriver getKeyDeriver() { 31 | return new Pbkdf2KeyDeriver(); 32 | } 33 | 34 | @Override 35 | public KeyDeriver getKeyDeriver(byte[] salt) { 36 | return new Pbkdf2KeyDeriver(salt); 37 | } 38 | }, 39 | SCRYPT() { 40 | public KeyDeriver getKeyDeriver() { 41 | return new ScryptKeyDeriver(); 42 | } 43 | 44 | @Override 45 | public KeyDeriver getKeyDeriver(byte[] salt) { 46 | return new ScryptKeyDeriver(salt); 47 | } 48 | }, 49 | ARGON2() { 50 | public KeyDeriver getKeyDeriver() { 51 | return new Argon2KeyDeriver(); 52 | } 53 | 54 | public KeyDeriver getKeyDeriver(byte[] salt) { 55 | return new Argon2KeyDeriver(salt); 56 | } 57 | }; 58 | 59 | public abstract KeyDeriver getKeyDeriver(); 60 | 61 | public KeyDeriver getKeyDeriver(byte[] salt) { 62 | return getKeyDeriver(); 63 | } 64 | } 65 | 66 | public enum Crypter { 67 | NONE() { 68 | @Override 69 | public KeyCrypter getKeyCrypter() { 70 | return new KeyCrypter() { 71 | @Override 72 | public byte[] decrypt(EncryptedData encryptedBytesToDecode, Key key) throws KeyCrypterException { 73 | return encryptedBytesToDecode.getEncryptedBytes(); 74 | } 75 | 76 | @Override 77 | public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, Key key) throws KeyCrypterException { 78 | return new EncryptedData(plainBytes, initializationVector, key.getSalt(), key.getDeriver(), NONE); 79 | } 80 | 81 | @Override 82 | public Crypter getCrypterType() { 83 | return NONE; 84 | } 85 | }; 86 | } 87 | }, 88 | AES_CBC_PKCS7() { 89 | @Override 90 | public KeyCrypter getKeyCrypter() { 91 | return new AESKeyCrypter(); 92 | } 93 | }; 94 | 95 | public abstract KeyCrypter getKeyCrypter(); 96 | } 97 | 98 | private final Deriver deriver; 99 | private final Crypter crypter; 100 | 101 | public EncryptionType(Deriver deriver, Crypter crypter) { 102 | this.deriver = deriver; 103 | this.crypter = crypter; 104 | } 105 | 106 | public Deriver getDeriver() { 107 | return deriver; 108 | } 109 | 110 | public Crypter getCrypter() { 111 | return crypter; 112 | } 113 | 114 | @Override 115 | public boolean equals(Object o) { 116 | if (this == o) return true; 117 | if (o == null || getClass() != o.getClass()) return false; 118 | EncryptionType that = (EncryptionType) o; 119 | return deriver == that.deriver && 120 | crypter == that.crypter; 121 | } 122 | 123 | @Override 124 | public int hashCode() { 125 | return Objects.hash(deriver, crypter); 126 | } 127 | 128 | @Override 129 | public String toString() { 130 | return "EncryptionType[" + 131 | "deriver=" + deriver + 132 | ", crypter=" + crypter + 133 | ']'; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/HDDerivationException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | public class HDDerivationException extends RuntimeException { 4 | public HDDerivationException() { 5 | super(); 6 | } 7 | 8 | public HDDerivationException(String message) { 9 | super(message); 10 | } 11 | 12 | public HDDerivationException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public HDDerivationException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/InvalidPasswordException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | public class InvalidPasswordException extends RuntimeException { 4 | public InvalidPasswordException() { 5 | super(); 6 | } 7 | 8 | public InvalidPasswordException(String message) { 9 | super(message); 10 | } 11 | 12 | public InvalidPasswordException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public InvalidPasswordException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/Key.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import java.util.Arrays; 4 | 5 | public class Key { 6 | private final byte[] keyBytes; 7 | private final byte[] salt; 8 | private final EncryptionType.Deriver deriver; 9 | 10 | public Key(byte[] keyBytes, byte[] salt, EncryptionType.Deriver deriver) { 11 | this.keyBytes = keyBytes; 12 | this.salt = salt; 13 | this.deriver = deriver; 14 | } 15 | 16 | public byte[] getKeyBytes() { 17 | return keyBytes; 18 | } 19 | 20 | public byte[] getSalt() { 21 | return salt; 22 | } 23 | 24 | public EncryptionType.Deriver getDeriver() { 25 | return deriver; 26 | } 27 | 28 | public void clear() { 29 | Arrays.fill(keyBytes, (byte)0); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/KeyCrypter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | public interface KeyCrypter { 4 | /** 5 | * Decrypt the provided encrypted bytes, converting them into unencrypted bytes. 6 | * 7 | * @throws KeyCrypterException if decryption was unsuccessful. 8 | */ 9 | byte[] decrypt(EncryptedData encryptedBytesToDecode, Key key) throws KeyCrypterException; 10 | 11 | /** 12 | * Encrypt the supplied bytes, converting them into ciphertext. 13 | * 14 | * @return EncryptedData An EncryptedData object containing the encrypted bytes and an initialisation vector and key salt. 15 | * @throws KeyCrypterException if encryption was unsuccessful 16 | */ 17 | EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, Key key) throws KeyCrypterException; 18 | 19 | EncryptionType.Crypter getCrypterType(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/KeyCrypterException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | /** 4 | *

Exception to provide the following:

5 | *
    6 | *
  • Provision of encryption / decryption exception
  • 7 | *
8 | *

This base exception acts as a general failure mode not attributable to a specific cause (other than 9 | * that reported in the exception message). Since this is in English, it may not be worth reporting directly 10 | * to the user other than as part of a "general failure to parse" response.

11 | */ 12 | public class KeyCrypterException extends RuntimeException { 13 | public KeyCrypterException(String s) { 14 | super(s); 15 | } 16 | 17 | public KeyCrypterException(String s, Throwable throwable) { 18 | super(s, throwable); 19 | } 20 | 21 | /** 22 | * This exception is thrown when a private key or seed is decrypted, it doesn't match its public key any 23 | * more. This likely means the wrong decryption key has been used. 24 | */ 25 | public static class PublicPrivateMismatch extends KeyCrypterException { 26 | public PublicPrivateMismatch(String message) { 27 | super(message); 28 | } 29 | 30 | public PublicPrivateMismatch(String message, Throwable throwable) { 31 | super(message, throwable); 32 | } 33 | } 34 | 35 | /** 36 | * This exception is thrown when a private key or seed is decrypted, the decrypted message is damaged 37 | * (e.g. the padding is damaged). This likely means the wrong decryption key has been used. 38 | */ 39 | public static class InvalidCipherText extends KeyCrypterException { 40 | public InvalidCipherText(String message) { 41 | super(message); 42 | } 43 | 44 | public InvalidCipherText(String message, Throwable throwable) { 45 | super(message, throwable); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/KeyDeriver.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | public interface KeyDeriver { 4 | /** 5 | * Create a Key (which typically contains an AES key) 6 | * @param password 7 | * @return Key The Key which typically contains the AES key to use for encrypting and decrypting 8 | * @throws KeyCrypterException 9 | */ 10 | Key deriveKey(CharSequence password) throws KeyCrypterException; 11 | 12 | EncryptionType.Deriver getDeriverType(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/LazyECPoint.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import org.bouncycastle.math.ec.ECCurve; 4 | import org.bouncycastle.math.ec.ECPoint; 5 | import org.bouncycastle.util.encoders.Hex; 6 | 7 | import java.util.Arrays; 8 | 9 | public class LazyECPoint { 10 | // If curve is set, bits is also set. If curve is unset, point is set and bits is unset. Point can be set along 11 | // with curve and bits when the cached form has been accessed and thus must have been converted. 12 | 13 | private final ECCurve curve; 14 | private final byte[] bits; 15 | private final boolean compressed; 16 | 17 | // This field is effectively final - once set it won't change again. However it can be set after 18 | // construction. 19 | private ECPoint point; 20 | 21 | public LazyECPoint(ECCurve curve, byte[] bits) { 22 | this.curve = curve; 23 | this.bits = (bits != null && bits.length == 32 ? addYCoord(bits) : bits); 24 | this.compressed = ECKey.isPubKeyCompressed(bits); 25 | } 26 | 27 | public LazyECPoint(ECPoint point, boolean compressed) { 28 | this.point = point; 29 | this.compressed = compressed; 30 | this.curve = null; 31 | this.bits = null; 32 | } 33 | 34 | public ECPoint get() { 35 | if (point == null) 36 | point = curve.decodePoint(bits); 37 | return point; 38 | } 39 | 40 | // Delegated methods. 41 | 42 | public ECPoint getDetachedPoint() { 43 | return get().getDetachedPoint(); 44 | } 45 | 46 | public boolean isCompressed() { 47 | return compressed; 48 | } 49 | 50 | public byte[] getEncoded() { 51 | if (bits != null) 52 | return Arrays.copyOf(bits, bits.length); 53 | else 54 | return get().getEncoded(compressed); 55 | } 56 | 57 | public byte[] getEncoded(boolean compressed) { 58 | if (compressed == isCompressed() && bits != null) 59 | return Arrays.copyOf(bits, bits.length); 60 | else 61 | return get().getEncoded(compressed); 62 | } 63 | 64 | public byte[] getEncodedXCoord() { 65 | byte[] compressed = getEncoded(true); 66 | byte[] xcoord = new byte[32]; 67 | System.arraycopy(compressed, 1, xcoord, 0, 32); 68 | return xcoord; 69 | } 70 | 71 | public String toString() { 72 | return Hex.toHexString(getEncoded()); 73 | } 74 | 75 | @Override 76 | public boolean equals(Object o) { 77 | if (this == o) return true; 78 | if (o == null || getClass() != o.getClass()) return false; 79 | return Arrays.equals(getCanonicalEncoding(), ((LazyECPoint)o).getCanonicalEncoding()); 80 | } 81 | 82 | @Override 83 | public int hashCode() { 84 | return Arrays.hashCode(getCanonicalEncoding()); 85 | } 86 | 87 | private byte[] getCanonicalEncoding() { 88 | return getEncoded(true); 89 | } 90 | 91 | private static byte[] addYCoord(byte[] xcoord) { 92 | byte[] compressed = new byte[33]; 93 | compressed[0] = 0x02; 94 | System.arraycopy(xcoord, 0, compressed, 1, 32); 95 | return compressed; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/Pbkdf2KeyDeriver.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import com.sparrowwallet.drongo.SecureString; 4 | import org.bouncycastle.crypto.digests.SHA512Digest; 5 | import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; 6 | import org.bouncycastle.crypto.params.KeyParameter; 7 | 8 | public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver { 9 | public static final int DEFAULT_ITERATION_COUNT = 1024; 10 | public static final int DEFAULT_KEY_SIZE = 512; 11 | 12 | private final byte[] salt; 13 | private final int iterationCount; 14 | private final int keySize; 15 | 16 | public static final Pbkdf2KeyDeriver DEFAULT_INSTANCE = new Pbkdf2KeyDeriver(); 17 | 18 | public Pbkdf2KeyDeriver() { 19 | this.salt = new byte[0]; 20 | this.iterationCount = DEFAULT_ITERATION_COUNT; 21 | this.keySize = DEFAULT_KEY_SIZE; 22 | } 23 | 24 | public Pbkdf2KeyDeriver(byte[] salt) { 25 | this.salt = salt; 26 | this.iterationCount = DEFAULT_ITERATION_COUNT; 27 | this.keySize = DEFAULT_KEY_SIZE; 28 | } 29 | 30 | public Pbkdf2KeyDeriver(byte[] salt, int iterationCount) { 31 | this.salt = salt; 32 | this.iterationCount = iterationCount; 33 | this.keySize = DEFAULT_KEY_SIZE; 34 | } 35 | 36 | public Pbkdf2KeyDeriver(byte[] salt, int iterationCount, int keySize) { 37 | this.salt = salt; 38 | this.iterationCount = iterationCount; 39 | this.keySize = keySize; 40 | } 41 | 42 | @Override 43 | public byte[] getSalt() { 44 | return salt; 45 | } 46 | 47 | @Override 48 | public Key deriveKey(CharSequence password) throws KeyCrypterException { 49 | PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest()); 50 | gen.init(SecureString.toBytesUTF8(password), salt, iterationCount); 51 | byte[] keyBytes = ((KeyParameter)gen.generateDerivedParameters(keySize)).getKey(); 52 | return new Key(keyBytes, salt, getDeriverType()); 53 | } 54 | 55 | @Override 56 | public ECKey deriveECKey(CharSequence password) throws KeyCrypterException { 57 | Key key = deriveKey(password); 58 | return ECKey.fromPrivate(key.getKeyBytes()); 59 | } 60 | 61 | @Override 62 | public EncryptionType.Deriver getDeriverType() { 63 | return EncryptionType.Deriver.PBKDF2; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/SchnorrSignature.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import com.sparrowwallet.drongo.Utils; 4 | import com.sparrowwallet.drongo.protocol.TransactionSignature; 5 | import org.bitcoin.NativeSecp256k1; 6 | import org.bitcoin.NativeSecp256k1Util; 7 | import org.bitcoin.Secp256k1Context; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.math.BigInteger; 12 | import java.nio.ByteBuffer; 13 | import java.util.Arrays; 14 | import java.util.Objects; 15 | 16 | /** 17 | * Groups the two components that make up a Schnorr signature 18 | */ 19 | public class SchnorrSignature { 20 | private static final Logger log = LoggerFactory.getLogger(SchnorrSignature.class); 21 | 22 | /** 23 | * The two components of the signature. 24 | */ 25 | public final BigInteger r, s; 26 | 27 | /** 28 | * Constructs a signature with the given components. Does NOT automatically canonicalise the signature. 29 | */ 30 | public SchnorrSignature(BigInteger r, BigInteger s) { 31 | this.r = r; 32 | this.s = s; 33 | } 34 | 35 | public byte[] encode() { 36 | ByteBuffer buffer = ByteBuffer.allocate(64); 37 | buffer.put(Utils.bigIntegerToBytes(r, 32)); 38 | buffer.put(Utils.bigIntegerToBytes(s, 32)); 39 | return buffer.array(); 40 | } 41 | 42 | public static SchnorrSignature decode(byte[] bytes) { 43 | if(bytes.length != 64) { 44 | throw new IllegalArgumentException("Invalid Schnorr signature length of " + bytes.length + " bytes"); 45 | } 46 | 47 | BigInteger r = new BigInteger(1, Arrays.copyOfRange(bytes, 0, 32)); 48 | BigInteger s = new BigInteger(1, Arrays.copyOfRange(bytes, 32, 64)); 49 | 50 | return new SchnorrSignature(r, s); 51 | } 52 | 53 | public static TransactionSignature decodeFromBitcoin(byte[] bytes) { 54 | if(bytes.length < 64 || bytes.length > 65) { 55 | throw new IllegalArgumentException("Invalid Schnorr signature length of " + bytes.length + " bytes"); 56 | } 57 | 58 | BigInteger r = new BigInteger(1, Arrays.copyOfRange(bytes, 0, 32)); 59 | BigInteger s = new BigInteger(1, Arrays.copyOfRange(bytes, 32, 64)); 60 | 61 | if(bytes.length == 65) { 62 | return new TransactionSignature(r, s, TransactionSignature.Type.SCHNORR, bytes[64]); 63 | } 64 | 65 | return new TransactionSignature(r, s, TransactionSignature.Type.SCHNORR, (byte)0); 66 | } 67 | 68 | public boolean verify(byte[] data, byte[] pub) { 69 | if(!Secp256k1Context.isEnabled()) { 70 | throw new IllegalStateException("libsecp256k1 is not enabled"); 71 | } 72 | 73 | try { 74 | return NativeSecp256k1.schnorrVerify(encode(), data, pub); 75 | } catch(NativeSecp256k1Util.AssertFailException e) { 76 | log.error("Error verifying schnorr signature", e); 77 | } 78 | 79 | return false; 80 | } 81 | 82 | @Override 83 | public boolean equals(Object o) { 84 | if(this == o) { 85 | return true; 86 | } 87 | if(o == null || getClass() != o.getClass()) { 88 | return false; 89 | } 90 | SchnorrSignature that = (SchnorrSignature) o; 91 | return r.equals(that.r) && s.equals(that.s); 92 | } 93 | 94 | @Override 95 | public int hashCode() { 96 | return Objects.hash(r, s); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/Secp256r1Key.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import org.bouncycastle.asn1.x9.ECNamedCurveTable; 4 | import org.bouncycastle.asn1.x9.X9ECParameters; 5 | import org.bouncycastle.crypto.params.ECDomainParameters; 6 | import org.bouncycastle.crypto.params.ECPublicKeyParameters; 7 | import org.bouncycastle.crypto.signers.ECDSASigner; 8 | import org.bouncycastle.math.ec.ECPoint; 9 | 10 | import java.math.BigInteger; 11 | 12 | public class Secp256r1Key { 13 | private static final X9ECParameters CURVE_PARAMS = ECNamedCurveTable.getByName("P-256"); 14 | 15 | public static final ECDomainParameters CURVE; 16 | 17 | private final ECPoint point; 18 | 19 | static { 20 | CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH()); 21 | } 22 | 23 | public Secp256r1Key(byte[] publicKeyBytes) { 24 | this.point = CURVE.getCurve().decodePoint(publicKeyBytes); 25 | } 26 | 27 | public boolean verify(byte[] challenge, byte[] challengeSignature) { 28 | ECDSASigner signer = new ECDSASigner(); 29 | signer.init(false, new ECPublicKeyParameters(point, CURVE)); 30 | 31 | int halfLength = challengeSignature.length / 2; 32 | byte[] r = new byte[halfLength]; 33 | byte[] s = new byte[halfLength]; 34 | System.arraycopy(challengeSignature, 0, r, 0, halfLength); 35 | System.arraycopy(challengeSignature, halfLength, s, 0, halfLength); 36 | 37 | return signer.verifySignature(challenge, new BigInteger(1, r), new BigInteger(1, s)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/VersionedChecksummedBytes.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | 4 | import com.sparrowwallet.drongo.Utils; 5 | import com.sparrowwallet.drongo.protocol.Base58; 6 | import com.sparrowwallet.drongo.protocol.Sha256Hash; 7 | 8 | import java.io.Serializable; 9 | import java.util.Arrays; 10 | import java.util.Objects; 11 | 12 | /** 13 | *

In Bitcoin the following format is often used to represent some type of key:

14 | *

15 | *

[one version byte] [data bytes] [4 checksum bytes]
16 | *

17 | *

and the result is then Base58 encoded. This format is used for addresses, and private keys exported using the 18 | * dumpprivkey command.

19 | */ 20 | public class VersionedChecksummedBytes implements Serializable, Cloneable, Comparable { 21 | protected final int version; 22 | protected byte[] bytes; 23 | 24 | protected VersionedChecksummedBytes(String encoded) { 25 | byte[] versionAndDataBytes = Base58.decodeChecked(encoded); 26 | byte versionByte = versionAndDataBytes[0]; 27 | version = versionByte & 0xFF; 28 | bytes = new byte[versionAndDataBytes.length - 1]; 29 | System.arraycopy(versionAndDataBytes, 1, bytes, 0, versionAndDataBytes.length - 1); 30 | } 31 | 32 | protected VersionedChecksummedBytes(int version, byte[] bytes) { 33 | this.version = version; 34 | this.bytes = bytes; 35 | } 36 | 37 | /** 38 | * Returns the base-58 encoded String representation of this 39 | * object, including version and checksum bytes. 40 | */ 41 | public final String toBase58() { 42 | // A stringified buffer is: 43 | // 1 byte version + data bytes + 4 bytes check code (a truncated hash) 44 | byte[] addressBytes = new byte[1 + bytes.length + 4]; 45 | addressBytes[0] = (byte) version; 46 | System.arraycopy(bytes, 0, addressBytes, 1, bytes.length); 47 | byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, bytes.length + 1); 48 | System.arraycopy(checksum, 0, addressBytes, bytes.length + 1, 4); 49 | return Base58.encode(addressBytes); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return toBase58(); 55 | } 56 | 57 | @Override 58 | public boolean equals(Object o) { 59 | if(this == o) { 60 | return true; 61 | } 62 | if(o == null || getClass() != o.getClass()) { 63 | return false; 64 | } 65 | VersionedChecksummedBytes that = (VersionedChecksummedBytes) o; 66 | return version == that.version && Arrays.equals(bytes, that.bytes); 67 | } 68 | 69 | @Override 70 | public int hashCode() { 71 | int result = Objects.hash(version); 72 | result = 31 * result + Arrays.hashCode(bytes); 73 | return result; 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | * 79 | * This implementation narrows the return type to VersionedChecksummedBytes 80 | * and allows subclasses to throw CloneNotSupportedException even though it 81 | * is never thrown by this implementation. 82 | */ 83 | @Override 84 | public VersionedChecksummedBytes clone() throws CloneNotSupportedException { 85 | return (VersionedChecksummedBytes) super.clone(); 86 | } 87 | 88 | /** 89 | * {@inheritDoc} 90 | * 91 | * This implementation uses an optimized Google Guava method to compare bytes. 92 | */ 93 | @Override 94 | public int compareTo(VersionedChecksummedBytes o) { 95 | int result = Integer.compare(this.version, o.version); 96 | Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator(); 97 | return result != 0 ? result : lexicographicByteArrayComparator.compare(this.bytes, o.bytes); 98 | } 99 | 100 | /** 101 | * Returns the "version" or "header" byte: the first byte of the data. This is used to disambiguate what the 102 | * contents apply to, for example, which network the key or address is valid on. 103 | * 104 | * @return A positive number between 0 and 255. 105 | */ 106 | public int getVersion() { 107 | return version; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/crypto/X25519Key.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import org.bouncycastle.asn1.ASN1ObjectIdentifier; 4 | import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 5 | import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; 6 | import org.bouncycastle.crypto.params.X25519PrivateKeyParameters; 7 | import org.bouncycastle.crypto.params.X25519PublicKeyParameters; 8 | 9 | import java.io.IOException; 10 | import java.math.BigInteger; 11 | import java.security.*; 12 | import java.security.interfaces.XECPrivateKey; 13 | import java.security.interfaces.XECPublicKey; 14 | import java.security.spec.AlgorithmParameterSpec; 15 | import java.util.Optional; 16 | 17 | public class X25519Key { 18 | private final KeyPair keyPair; 19 | private final AlgorithmParameterSpec ecSpec; 20 | 21 | public X25519Key() { 22 | this(generatePrivateKey()); 23 | } 24 | 25 | public X25519Key(byte[] priv) { 26 | try { 27 | final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("X25519"); 28 | this.ecSpec = keyPairGenerator.generateKeyPair().getPrivate().getParams(); 29 | } catch(NoSuchAlgorithmException e) { 30 | throw new RuntimeException(e); 31 | } 32 | 33 | X25519PrivateKeyParameters privateKeyParams = new X25519PrivateKeyParameters(priv, 0); 34 | X25519PublicKeyParameters publicKeyParams = privateKeyParams.generatePublicKey(); 35 | 36 | PrivateKey privateKey = new BouncyCastlePrivateKey(privateKeyParams); 37 | PublicKey publicKey = new BouncyCastlePublicKey(publicKeyParams); 38 | this.keyPair = new KeyPair(publicKey, privateKey); 39 | } 40 | 41 | public KeyPair getKeyPair() { 42 | return keyPair; 43 | } 44 | 45 | public byte[] getRawPrivateKeyBytes() { 46 | return keyPair.getPrivate().getEncoded(); 47 | } 48 | 49 | private static byte[] generatePrivateKey() { 50 | SecureRandom secureRandom = new SecureRandom(); 51 | byte[] privateKey = new byte[32]; 52 | secureRandom.nextBytes(privateKey); 53 | return privateKey; 54 | } 55 | 56 | public class BouncyCastlePrivateKey implements XECPrivateKey { 57 | private final X25519PrivateKeyParameters privateKeyParams; 58 | 59 | BouncyCastlePrivateKey(X25519PrivateKeyParameters privateKeyParams) { 60 | this.privateKeyParams = privateKeyParams; 61 | } 62 | 63 | @Override 64 | public String getAlgorithm() { 65 | return "X25519"; 66 | } 67 | 68 | @Override 69 | public String getFormat() { 70 | return "RAW"; 71 | } 72 | 73 | @Override 74 | public byte[] getEncoded() { 75 | return privateKeyParams.getEncoded(); 76 | } 77 | 78 | @Override 79 | public Optional getScalar() { 80 | return Optional.of(getEncoded()); 81 | } 82 | 83 | @Override 84 | public AlgorithmParameterSpec getParams() { 85 | return ecSpec; 86 | } 87 | } 88 | 89 | public class BouncyCastlePublicKey implements XECPublicKey { 90 | private final X25519PublicKeyParameters publicKeyParams; 91 | 92 | BouncyCastlePublicKey(X25519PublicKeyParameters publicKeyParams) { 93 | this.publicKeyParams = publicKeyParams; 94 | } 95 | 96 | @Override 97 | public String getAlgorithm() { 98 | return "X25519"; 99 | } 100 | 101 | @Override 102 | public String getFormat() { 103 | return "X.509"; 104 | } 105 | 106 | @Override 107 | public byte[] getEncoded() { 108 | try { 109 | ASN1ObjectIdentifier algOid = new ASN1ObjectIdentifier("1.3.101.110"); 110 | AlgorithmIdentifier algId = new AlgorithmIdentifier(algOid); 111 | SubjectPublicKeyInfo spki = new SubjectPublicKeyInfo(algId, publicKeyParams.getEncoded()); 112 | return spki.getEncoded(); 113 | } catch(IOException e) { 114 | throw new RuntimeException(e); 115 | } 116 | } 117 | 118 | @Override 119 | public BigInteger getU() { 120 | return new BigInteger(1, publicKeyParams.getEncoded()); 121 | } 122 | 123 | @Override 124 | public AlgorithmParameterSpec getParams() { 125 | return ecSpec; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/pgp/PGPKeySource.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.pgp; 2 | 3 | public enum PGPKeySource { 4 | USER, GPG, APPLICATION, NONE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/pgp/PGPVerificationException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.pgp; 2 | 3 | public class PGPVerificationException extends Exception { 4 | public PGPVerificationException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/pgp/PGPVerificationResult.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.pgp; 2 | 3 | import java.util.Date; 4 | 5 | public record PGPVerificationResult(long keyId, String userId, String fingerprint, Date signatureTimestamp, boolean expired, PGPKeySource keySource) { } 6 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/policy/Miniscript.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.policy; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public class Miniscript { 7 | private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\("); 8 | private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\("); 9 | private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\((\\d+)"); 10 | 11 | private String script; 12 | 13 | public Miniscript(String script) { 14 | this.script = script; 15 | } 16 | 17 | public String getScript() { 18 | return script; 19 | } 20 | 21 | public void setScript(String script) { 22 | this.script = script; 23 | } 24 | 25 | public int getNumSignaturesRequired() { 26 | Matcher singleMatcher = SINGLE_PATTERN.matcher(script); 27 | if(singleMatcher.find()) { 28 | return 1; 29 | } 30 | 31 | Matcher taprootMatcher = TAPROOT_PATTERN.matcher(script); 32 | if(taprootMatcher.find()) { 33 | return 1; 34 | } 35 | 36 | Matcher multiMatcher = MULTI_PATTERN.matcher(script); 37 | if(multiMatcher.find()) { 38 | String threshold = multiMatcher.group(1); 39 | return Integer.parseInt(threshold); 40 | } else { 41 | throw new IllegalArgumentException("Could not find multisig threshold in " + this); 42 | } 43 | } 44 | 45 | public Miniscript copy() { 46 | return new Miniscript(script); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return getScript(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/policy/Policy.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.policy; 2 | 3 | import com.sparrowwallet.drongo.protocol.ScriptType; 4 | import com.sparrowwallet.drongo.wallet.Keystore; 5 | import com.sparrowwallet.drongo.wallet.Persistable; 6 | 7 | import java.util.List; 8 | 9 | import static com.sparrowwallet.drongo.protocol.ScriptType.*; 10 | import static com.sparrowwallet.drongo.policy.PolicyType.*; 11 | 12 | public class Policy extends Persistable { 13 | private static final String DEFAULT_NAME = "Default"; 14 | 15 | private String name; 16 | private Miniscript miniscript; 17 | 18 | public Policy(Miniscript miniscript) { 19 | this(DEFAULT_NAME, miniscript); 20 | } 21 | 22 | public Policy(String name, Miniscript miniscript) { 23 | this.name = name; 24 | this.miniscript = miniscript; 25 | } 26 | 27 | public String getName() { 28 | return name; 29 | } 30 | 31 | public Miniscript getMiniscript() { 32 | return miniscript; 33 | } 34 | 35 | public void setMiniscript(Miniscript miniscript) { 36 | this.miniscript = miniscript; 37 | } 38 | 39 | public int getNumSignaturesRequired() { 40 | return getMiniscript().getNumSignaturesRequired(); 41 | } 42 | 43 | public static Policy getPolicy(PolicyType policyType, ScriptType scriptType, List keystores, Integer threshold) { 44 | if(SINGLE.equals(policyType)) { 45 | return new Policy(new Miniscript(scriptType.getDescriptor() + keystores.get(0).getScriptName() + scriptType.getCloseDescriptor())); 46 | } 47 | 48 | if(MULTI.equals(policyType)) { 49 | StringBuilder builder = new StringBuilder(); 50 | builder.append(scriptType.getDescriptor()); 51 | builder.append(MULTISIG.getDescriptor()); 52 | builder.append(threshold); 53 | for(Keystore keystore : keystores) { 54 | builder.append(",").append(keystore.getScriptName()); 55 | } 56 | builder.append(MULTISIG.getCloseDescriptor()); 57 | builder.append(scriptType.getCloseDescriptor()); 58 | return new Policy(new Miniscript(builder.toString())); 59 | } 60 | 61 | throw new PolicyException("No standard policy for " + policyType + " policy with script type " + scriptType); 62 | } 63 | 64 | public Policy copy() { 65 | Policy policy = new Policy(name, miniscript.copy()); 66 | policy.setId(getId()); 67 | return policy; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/policy/PolicyException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.policy; 2 | 3 | public class PolicyException extends RuntimeException { 4 | public PolicyException() { 5 | } 6 | 7 | public PolicyException(String message) { 8 | super(message); 9 | } 10 | 11 | public PolicyException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public PolicyException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public PolicyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/policy/PolicyType.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.policy; 2 | 3 | import com.sparrowwallet.drongo.protocol.ScriptType; 4 | 5 | import static com.sparrowwallet.drongo.protocol.ScriptType.*; 6 | 7 | public enum PolicyType { 8 | SINGLE("Single Signature", P2WPKH), MULTI("Multi Signature", P2WSH), CUSTOM("Custom", P2WSH); 9 | 10 | private String name; 11 | private ScriptType defaultScriptType; 12 | 13 | PolicyType(String name, ScriptType defaultScriptType) { 14 | this.name = name; 15 | this.defaultScriptType = defaultScriptType; 16 | } 17 | 18 | public String getName() { 19 | return name; 20 | } 21 | 22 | public ScriptType getDefaultScriptType() { 23 | return defaultScriptType; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return name; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/BlockHeader.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | import java.util.Date; 7 | 8 | import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE; 9 | 10 | public class BlockHeader extends Message { 11 | private long version; 12 | private Sha256Hash prevBlockHash; 13 | private Sha256Hash merkleRoot, witnessRoot; 14 | private long time; 15 | private long difficultyTarget; // "nBits" 16 | private long nonce; 17 | 18 | public BlockHeader(byte[] rawheader) { 19 | super(rawheader, 0); 20 | } 21 | 22 | public BlockHeader(long version, Sha256Hash prevBlockHash, Sha256Hash merkleRoot, Sha256Hash witnessRoot, long time, long difficultyTarget, long nonce) { 23 | this.version = version; 24 | this.prevBlockHash = prevBlockHash; 25 | this.merkleRoot = merkleRoot; 26 | this.witnessRoot = witnessRoot; 27 | this.time = time; 28 | this.difficultyTarget = difficultyTarget; 29 | this.nonce = nonce; 30 | } 31 | 32 | @Override 33 | protected void parse() throws ProtocolException { 34 | version = readUint32(); 35 | prevBlockHash = readHash(); 36 | merkleRoot = readHash(); 37 | time = readUint32(); 38 | difficultyTarget = readUint32(); 39 | nonce = readUint32(); 40 | 41 | length = cursor - offset; 42 | } 43 | 44 | public long getVersion() { 45 | return version; 46 | } 47 | 48 | public Sha256Hash getPrevBlockHash() { 49 | return prevBlockHash; 50 | } 51 | 52 | public Sha256Hash getMerkleRoot() { 53 | return merkleRoot; 54 | } 55 | 56 | public Sha256Hash getWitnessRoot() { 57 | return witnessRoot; 58 | } 59 | 60 | public long getTime() { 61 | return time; 62 | } 63 | 64 | public Date getTimeAsDate() { 65 | return new Date(time * 1000); 66 | } 67 | 68 | public long getDifficultyTarget() { 69 | return difficultyTarget; 70 | } 71 | 72 | public long getNonce() { 73 | return nonce; 74 | } 75 | 76 | public byte[] bitcoinSerialize() { 77 | try { 78 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 79 | bitcoinSerializeToStream(outputStream); 80 | return outputStream.toByteArray(); 81 | } catch (IOException e) { 82 | //can't happen 83 | } 84 | 85 | return null; 86 | } 87 | 88 | protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { 89 | uint32ToByteStreamLE(version, stream); 90 | stream.write(prevBlockHash.getReversedBytes()); 91 | stream.write(merkleRoot.getReversedBytes()); 92 | uint32ToByteStreamLE(time, stream); 93 | uint32ToByteStreamLE(difficultyTarget, stream); 94 | uint32ToByteStreamLE(nonce, stream); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/ChildMessage.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | public abstract class ChildMessage extends Message { 4 | 5 | protected Message parent; 6 | 7 | protected ChildMessage() { 8 | super(); 9 | } 10 | 11 | protected ChildMessage(byte[] rawtx, int offset) { 12 | super(rawtx, offset); 13 | } 14 | 15 | public Message getParent() { 16 | return parent; 17 | } 18 | 19 | public final void setParent(Message parent) { 20 | this.parent = parent; 21 | } 22 | 23 | protected void adjustLength(int adjustment) { 24 | adjustLength(0, adjustment); 25 | } 26 | 27 | protected void adjustLength(int newArraySize, int adjustment) { 28 | super.adjustLength(newArraySize, adjustment); 29 | 30 | if(parent != null) { 31 | parent.adjustLength(newArraySize, adjustment); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/HashIndex.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | public class HashIndex { 4 | private final Sha256Hash hash; 5 | private final long index; 6 | 7 | public HashIndex(Sha256Hash hash, long index) { 8 | this.hash = hash; 9 | this.index = index; 10 | } 11 | 12 | public Sha256Hash getHash() { 13 | return hash; 14 | } 15 | 16 | public long getIndex() { 17 | return index; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return hash.toString() + ":" + index; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if(this == o) { 28 | return true; 29 | } 30 | if(o == null || getClass() != o.getClass()) { 31 | return false; 32 | } 33 | 34 | HashIndex hashIndex = (HashIndex) o; 35 | 36 | if(index != hashIndex.index) { 37 | return false; 38 | } 39 | return hash.equals(hashIndex.hash); 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | int result = hash.hashCode(); 45 | result = 31 * result + (int) (index ^ (index >>> 32)); 46 | return result; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/NonStandardScriptException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | public class NonStandardScriptException extends Exception { 4 | public NonStandardScriptException() { 5 | super(); 6 | } 7 | 8 | public NonStandardScriptException(String message) { 9 | super(message); 10 | } 11 | 12 | public NonStandardScriptException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public NonStandardScriptException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/ProtocolException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | public class ProtocolException extends RuntimeException { 4 | public ProtocolException() { 5 | } 6 | 7 | public ProtocolException(String message) { 8 | super(message); 9 | } 10 | 11 | public ProtocolException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public ProtocolException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public ProtocolException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/SigHash.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * These constants are a part of a scriptSig signature on the inputs. They define the details of how a 7 | * transaction can be redeemed, specifically, they control how the hash of the transaction is calculated. 8 | */ 9 | public enum SigHash { 10 | ALL("All", (byte)1), 11 | NONE("None", (byte)2), 12 | SINGLE("Single", (byte)3), 13 | ANYONECANPAY("Anyone Can Pay", (byte)0x80), // Caution: Using this type in isolation is non-standard. Treated similar to ANYONECANPAY_ALL. 14 | ANYONECANPAY_ALL("All + Anyone Can Pay", (byte)0x81), 15 | ANYONECANPAY_NONE("None + Anyone Can Pay", (byte)0x82), 16 | ANYONECANPAY_SINGLE("Single + Anyone Can Pay", (byte)0x83), 17 | DEFAULT("Default", (byte)0); 18 | 19 | private final String name; 20 | public final byte value; 21 | 22 | public static final List LEGACY_SIGNING_TYPES = List.of(ALL, NONE, SINGLE, ANYONECANPAY_ALL, ANYONECANPAY_NONE, ANYONECANPAY_SINGLE); 23 | public static final List TAPROOT_SIGNING_TYPES = List.of(DEFAULT, ALL, NONE, SINGLE, ANYONECANPAY_ALL, ANYONECANPAY_NONE, ANYONECANPAY_SINGLE); 24 | 25 | private SigHash(final String name, final byte value) { 26 | this.name = name; 27 | this.value = value; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | /** 35 | * @return the value as a byte 36 | */ 37 | public byte byteValue() { 38 | return this.value; 39 | } 40 | 41 | public int intValue() { 42 | return Byte.toUnsignedInt(value); 43 | } 44 | 45 | public boolean anyoneCanPay() { 46 | return (value & SigHash.ANYONECANPAY.value) != 0; 47 | } 48 | 49 | public static SigHash fromByte(byte sigHashByte) { 50 | for(SigHash value : SigHash.values()) { 51 | if(sigHashByte == value.byteValue()) { 52 | return value; 53 | } 54 | } 55 | 56 | throw new IllegalArgumentException("No defined sighash value for byte " + sigHashByte); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return getName(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/SignatureDecodeException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | public class SignatureDecodeException extends RuntimeException { 4 | public SignatureDecodeException() { 5 | super(); 6 | } 7 | 8 | public SignatureDecodeException(String message) { 9 | super(message); 10 | } 11 | 12 | public SignatureDecodeException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public SignatureDecodeException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutPoint.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | import com.sparrowwallet.drongo.Utils; 4 | import com.sparrowwallet.drongo.address.Address; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | import java.util.Objects; 10 | 11 | public class TransactionOutPoint extends ChildMessage { 12 | 13 | static final int MESSAGE_LENGTH = 36; 14 | 15 | /** Hash of the transaction to which we refer. */ 16 | private Sha256Hash hash; 17 | /** Which output of that transaction we are talking about. */ 18 | private long index; 19 | 20 | private Address[] addresses = new Address[0]; 21 | 22 | public TransactionOutPoint(Sha256Hash hash, long index) { 23 | this.hash = hash; 24 | this.index = index; 25 | length = MESSAGE_LENGTH; 26 | } 27 | 28 | public TransactionOutPoint(byte[] rawtx, int offset, Message parent) { 29 | super(rawtx, offset); 30 | setParent(parent); 31 | } 32 | 33 | protected void parse() throws ProtocolException { 34 | length = MESSAGE_LENGTH; 35 | hash = readHash(); 36 | index = readUint32(); 37 | } 38 | 39 | public Sha256Hash getHash() { 40 | return hash; 41 | } 42 | 43 | public long getIndex() { 44 | return index; 45 | } 46 | 47 | public Address[] getAddresses() { 48 | return addresses; 49 | } 50 | 51 | public void setAddresses(Address[] addresses) { 52 | this.addresses = addresses; 53 | } 54 | 55 | public byte[] bitcoinSerialize() { 56 | try { 57 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 58 | bitcoinSerializeToStream(outputStream); 59 | return outputStream.toByteArray(); 60 | } catch (IOException e) { 61 | //can't happen 62 | } 63 | 64 | return null; 65 | } 66 | 67 | @Override 68 | protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { 69 | stream.write(hash.getReversedBytes()); 70 | Utils.uint32ToByteStreamLE(index, stream); 71 | } 72 | 73 | @Override 74 | public boolean equals(Object o) { 75 | if (this == o) return true; 76 | if (o == null || getClass() != o.getClass()) return false; 77 | TransactionOutPoint other = (TransactionOutPoint) o; 78 | return getIndex() == other.getIndex() && getHash().equals(other.getHash()); 79 | } 80 | 81 | @Override 82 | public int hashCode() { 83 | return Objects.hash(getIndex(), getHash()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | import com.sparrowwallet.drongo.Utils; 4 | import com.sparrowwallet.drongo.address.Address; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | 10 | public class TransactionOutput extends ChildMessage { 11 | private long value; 12 | private byte[] scriptBytes; 13 | private Script script; 14 | 15 | private Address[] addresses = new Address[0]; 16 | 17 | public TransactionOutput(Transaction transaction, long value, Script script) { 18 | this(transaction, value, script.getProgram()); 19 | } 20 | 21 | public TransactionOutput(Transaction transaction, long value, byte[] scriptBytes) { 22 | this.value = value; 23 | this.scriptBytes = scriptBytes; 24 | setParent(transaction); 25 | length = 8 + VarInt.sizeOf(scriptBytes.length) + scriptBytes.length; 26 | } 27 | 28 | public TransactionOutput(Transaction parent, byte[] rawtx, int offset) { 29 | super(rawtx, offset); 30 | setParent(parent); 31 | } 32 | 33 | protected void parse() throws ProtocolException { 34 | value = readInt64(); 35 | int scriptLen = (int) readVarInt(); 36 | length = cursor - offset + scriptLen; 37 | scriptBytes = readBytes(scriptLen); 38 | } 39 | 40 | public byte[] bitcoinSerialize() { 41 | try { 42 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 43 | bitcoinSerializeToStream(outputStream); 44 | return outputStream.toByteArray(); 45 | } catch (IOException e) { 46 | //can't happen 47 | } 48 | 49 | return null; 50 | } 51 | 52 | protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { 53 | Utils.int64ToByteStreamLE(value, stream); 54 | // TODO: Move script serialization into the Script class, where it belongs. 55 | stream.write(new VarInt(scriptBytes.length).encode()); 56 | stream.write(scriptBytes); 57 | } 58 | 59 | public byte[] getScriptBytes() { 60 | return scriptBytes; 61 | } 62 | 63 | public Script getScript() { 64 | if(script == null) { 65 | script = new Script(scriptBytes); 66 | } 67 | 68 | return script; 69 | } 70 | 71 | public long getValue() { 72 | return value; 73 | } 74 | 75 | public Address[] getAddresses() { 76 | return addresses; 77 | } 78 | 79 | public void setAddresses(Address[] addresses) { 80 | this.addresses = addresses; 81 | } 82 | 83 | public Sha256Hash getHash() { 84 | Transaction transaction = (Transaction)parent; 85 | return transaction.getTxId(); 86 | } 87 | 88 | public int getIndex() { 89 | Transaction transaction = (Transaction)parent; 90 | return transaction.getOutputs().indexOf(this); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/VarInt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.sparrowwallet.drongo.protocol; 18 | 19 | import com.sparrowwallet.drongo.Utils; 20 | 21 | /** 22 | * A variable-length encoded unsigned integer using Satoshi's encoding (a.k.a. "CompactSize"). 23 | */ 24 | public class VarInt { 25 | public final long value; 26 | private final int originallyEncodedSize; 27 | 28 | /** 29 | * Constructs a new VarInt with the given unsigned long value. 30 | * 31 | * @param value the unsigned long value (beware widening conversion of negatives!) 32 | */ 33 | public VarInt(long value) { 34 | this.value = value; 35 | originallyEncodedSize = getSizeInBytes(); 36 | } 37 | 38 | /** 39 | * Constructs a new VarInt with the value parsed from the specified offset of the given buffer. 40 | * 41 | * @param buf the buffer containing the value 42 | * @param offset the offset of the value 43 | */ 44 | public VarInt(byte[] buf, int offset) { 45 | int first = 0xFF & buf[offset]; 46 | if (first < 253) { 47 | value = first; 48 | originallyEncodedSize = 1; // 1 data byte (8 bits) 49 | } else if (first == 253) { 50 | value = Utils.readUint16(buf, offset + 1); 51 | originallyEncodedSize = 3; // 1 marker + 2 data bytes (16 bits) 52 | } else if (first == 254) { 53 | value = Utils.readUint32(buf, offset + 1); 54 | originallyEncodedSize = 5; // 1 marker + 4 data bytes (32 bits) 55 | } else { 56 | value = Utils.readInt64(buf, offset + 1); 57 | originallyEncodedSize = 9; // 1 marker + 8 data bytes (64 bits) 58 | } 59 | } 60 | 61 | /** 62 | * Returns the original number of bytes used to encode the value if it was 63 | * deserialized from a byte array, or the minimum encoded size if it was not. 64 | */ 65 | public int getOriginalSizeInBytes() { 66 | return originallyEncodedSize; 67 | } 68 | 69 | /** 70 | * Returns the minimum encoded size of the value. 71 | */ 72 | public final int getSizeInBytes() { 73 | return sizeOf(value); 74 | } 75 | 76 | /** 77 | * Returns the minimum encoded size of the given unsigned long value. 78 | * 79 | * @param value the unsigned long value (beware widening conversion of negatives!) 80 | */ 81 | public static int sizeOf(long value) { 82 | // if negative, it's actually a very large unsigned long value 83 | if (value < 0) return 9; // 1 marker + 8 data bytes 84 | if (value < 253) return 1; // 1 data byte 85 | if (value <= 0xFFFFL) return 3; // 1 marker + 2 data bytes 86 | if (value <= 0xFFFFFFFFL) return 5; // 1 marker + 4 data bytes 87 | return 9; // 1 marker + 8 data bytes 88 | } 89 | 90 | /** 91 | * Encodes the value into its minimal representation. 92 | * 93 | * @return the minimal encoded bytes of the value 94 | */ 95 | public byte[] encode() { 96 | byte[] bytes; 97 | switch (sizeOf(value)) { 98 | case 1: 99 | return new byte[]{(byte) value}; 100 | case 3: 101 | bytes = new byte[3]; 102 | bytes[0] = (byte) 253; 103 | Utils.uint16ToByteArrayLE((int) value, bytes, 1); 104 | return bytes; 105 | case 5: 106 | bytes = new byte[5]; 107 | bytes[0] = (byte) 254; 108 | Utils.uint32ToByteArrayLE(value, bytes, 1); 109 | return bytes; 110 | default: 111 | bytes = new byte[9]; 112 | bytes[0] = (byte) 255; 113 | Utils.int64ToByteArrayLE(value, bytes, 1); 114 | return bytes; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/protocol/VerificationException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.protocol; 2 | 3 | public class VerificationException extends RuntimeException { 4 | public VerificationException(String msg) { 5 | super(msg); 6 | } 7 | 8 | public VerificationException(Exception e) { 9 | super(e); 10 | } 11 | 12 | public VerificationException(String msg, Throwable t) { 13 | super(msg, t); 14 | } 15 | 16 | public static class EmptyInputsOrOutputs extends VerificationException { 17 | public EmptyInputsOrOutputs() { 18 | super("Transaction had no inputs or no outputs."); 19 | } 20 | } 21 | 22 | public static class LargerThanMaxBlockSize extends VerificationException { 23 | public LargerThanMaxBlockSize() { 24 | super("Transaction larger than MAX_BLOCK_SIZE"); 25 | } 26 | } 27 | 28 | public static class DuplicatedOutPoint extends VerificationException { 29 | public DuplicatedOutPoint() { 30 | super("Duplicated outpoint"); 31 | } 32 | } 33 | 34 | public static class NegativeValueOutput extends VerificationException { 35 | public NegativeValueOutput() { 36 | super("Transaction output negative"); 37 | } 38 | } 39 | 40 | public static class ExcessiveValue extends VerificationException { 41 | public ExcessiveValue() { 42 | super("Total transaction output value greater than possible"); 43 | } 44 | } 45 | 46 | public static class CoinbaseScriptSizeOutOfRange extends VerificationException { 47 | public CoinbaseScriptSizeOutOfRange() { 48 | super("Coinbase script size out of range"); 49 | } 50 | } 51 | 52 | public static class UnexpectedCoinbaseInput extends VerificationException { 53 | public UnexpectedCoinbaseInput() { 54 | super("Coinbase input as input in non-coinbase transaction"); 55 | } 56 | } 57 | 58 | public static class NoncanonicalSignature extends VerificationException { 59 | public NoncanonicalSignature() { 60 | super("Signature encoding is not canonical"); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/psbt/PSBTInputSigner.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.psbt; 2 | 3 | import com.sparrowwallet.drongo.crypto.ECKey; 4 | import com.sparrowwallet.drongo.protocol.Sha256Hash; 5 | import com.sparrowwallet.drongo.protocol.SigHash; 6 | import com.sparrowwallet.drongo.protocol.TransactionSignature; 7 | 8 | public interface PSBTInputSigner { 9 | TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType); 10 | ECKey getPubKey(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/psbt/PSBTParseException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.psbt; 2 | 3 | public class PSBTParseException extends Exception { 4 | public PSBTParseException() { 5 | super(); 6 | } 7 | 8 | public PSBTParseException(String message) { 9 | super(message); 10 | } 11 | 12 | public PSBTParseException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public PSBTParseException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/psbt/PSBTSignatureException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.psbt; 2 | 3 | public class PSBTSignatureException extends PSBTParseException { 4 | public PSBTSignatureException() { 5 | super(); 6 | } 7 | 8 | public PSBTSignatureException(String message) { 9 | super(message); 10 | } 11 | 12 | public PSBTSignatureException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public PSBTSignatureException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/uri/BitcoinURIParseException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.uri; 2 | 3 | /** 4 | *

Exception to provide the following to {@link BitcoinURI}:

5 | *
    6 | *
  • Provision of parsing error messages
  • 7 | *
8 | *

This base exception acts as a general failure mode not attributable to a specific cause (other than 9 | * that reported in the exception message). Since this is in English, it may not be worth reporting directly 10 | * to the user other than as part of a "general failure to parse" response.

11 | */ 12 | public class BitcoinURIParseException extends Exception { 13 | public BitcoinURIParseException(String s) { 14 | super(s); 15 | } 16 | 17 | public BitcoinURIParseException(String s, Throwable throwable) { 18 | super(s, throwable); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/uri/OptionalFieldValidationException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.uri; 2 | 3 | /** 4 | *

Exception to provide the following to {@link BitcoinURI}:

5 | *
    6 | *
  • Provision of parsing error messages
  • 7 | *
8 | *

This exception occurs when an optional field is detected (under the Bitcoin URI scheme) and fails 9 | * to pass the associated test (such as {@code amount} not being a valid number).

10 | * 11 | * @since 0.3.0 12 | * 13 | */ 14 | public class OptionalFieldValidationException extends BitcoinURIParseException { 15 | 16 | public OptionalFieldValidationException(String s) { 17 | super(s); 18 | } 19 | 20 | public OptionalFieldValidationException(String s, Throwable throwable) { 21 | super(s, throwable); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/uri/RequiredFieldValidationException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.uri; 2 | 3 | /** 4 | *

Exception to provide the following to {@link BitcoinURI}:

5 | *
    6 | *
  • Provision of parsing error messages
  • 7 | *
8 | *

This exception occurs when a required field is detected (under the BIP21 rules) and fails 9 | * to pass the associated test (such as {@code req-expires} being out of date), or the required field is unknown 10 | * to this version of the client in which case it should fail for security reasons.

11 | * 12 | * @since 0.3.0 13 | * 14 | */ 15 | public class RequiredFieldValidationException extends BitcoinURIParseException { 16 | 17 | public RequiredFieldValidationException(String s) { 18 | super(s); 19 | } 20 | 21 | public RequiredFieldValidationException(String s, Throwable throwable) { 22 | super(s, throwable); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/BlockTransaction.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.*; 4 | 5 | import java.util.Collections; 6 | import java.util.Date; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | public class BlockTransaction extends BlockTransactionHash implements Comparable { 11 | private final Transaction transaction; 12 | private final Sha256Hash blockHash; 13 | 14 | private final Set spending = new HashSet<>(); 15 | private final Set funding = new HashSet<>(); 16 | 17 | public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction) { 18 | this(hash, height, date, fee, transaction, null); 19 | } 20 | 21 | public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction, Sha256Hash blockHash) { 22 | this(hash, height, date, fee, transaction, blockHash, null); 23 | } 24 | 25 | public BlockTransaction(Sha256Hash hash, int height, Date date, Long fee, Transaction transaction, Sha256Hash blockHash, String label) { 26 | super(hash, height, date, fee, label); 27 | this.transaction = transaction; 28 | this.blockHash = blockHash; 29 | 30 | if(transaction != null) { 31 | for(TransactionInput txInput : transaction.getInputs()) { 32 | spending.add(new HashIndex(txInput.getOutpoint().getHash(), txInput.getOutpoint().getIndex())); 33 | } 34 | for(TransactionOutput txOutput : transaction.getOutputs()) { 35 | funding.add(new HashIndex(hash, txOutput.getIndex())); 36 | } 37 | } 38 | } 39 | 40 | public Transaction getTransaction() { 41 | return transaction; 42 | } 43 | 44 | public Sha256Hash getBlockHash() { 45 | return blockHash; 46 | } 47 | 48 | public Set getSpending() { 49 | return Collections.unmodifiableSet(spending); 50 | } 51 | 52 | public Set getFunding() { 53 | return Collections.unmodifiableSet(funding); 54 | } 55 | 56 | public Double getFeeRate() { 57 | if(getFee() != null && transaction != null) { 58 | double vSize = transaction.getVirtualSize(); 59 | return getFee() / vSize; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | @Override 66 | public int compareTo(BlockTransaction blkTx) { 67 | int blockOrder = compareBlockOrder(blkTx); 68 | if(blockOrder != 0) { 69 | return blockOrder; 70 | } 71 | 72 | return super.compareTo(blkTx); 73 | } 74 | 75 | public int compareBlockOrder(BlockTransaction blkTx) { 76 | if(getHeight() != blkTx.getHeight()) { 77 | return getComparisonHeight() - blkTx.getComparisonHeight(); 78 | } 79 | 80 | if(!Collections.disjoint(spending, blkTx.funding)) { 81 | return 1; 82 | } 83 | 84 | if(!Collections.disjoint(blkTx.spending, funding)) { 85 | return -1; 86 | } 87 | 88 | return 0; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/BlockTransactionHash.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.Sha256Hash; 4 | 5 | import java.util.Date; 6 | import java.util.Objects; 7 | 8 | public abstract class BlockTransactionHash extends Persistable { 9 | public static final int BLOCKS_TO_CONFIRM = 6; 10 | public static final int BLOCKS_TO_FULLY_CONFIRM = 100; 11 | 12 | private final Sha256Hash hash; 13 | private final int height; 14 | private final Date date; 15 | private final Long fee; 16 | 17 | private String label; 18 | 19 | public BlockTransactionHash(Sha256Hash hash, int height, Date date, Long fee, String label) { 20 | this.hash = hash; 21 | this.height = height; 22 | this.date = date; 23 | this.fee = fee; 24 | this.label = label; 25 | } 26 | 27 | public Sha256Hash getHash() { 28 | return hash; 29 | } 30 | 31 | public String getHashAsString() { 32 | return hash.toString(); 33 | } 34 | 35 | public int getHeight() { 36 | return height; 37 | } 38 | 39 | /** 40 | * Calculates a special height value that places txes with unconfirmed parents first, then normal unconfirmed txes, then confirmed txes 41 | * 42 | * @return the modified height value 43 | */ 44 | public int getComparisonHeight() { 45 | return (getHeight() > 0 ? getHeight() : (getHeight() == -1 ? Integer.MAX_VALUE : Integer.MAX_VALUE - getHeight() - 1)); 46 | } 47 | 48 | public int getConfirmations(int currentBlockHeight) { 49 | if(height <= 0) { 50 | return 0; 51 | } 52 | 53 | return currentBlockHeight - height + 1; 54 | } 55 | 56 | public Date getDate() { 57 | return date; 58 | } 59 | 60 | public Long getFee() { 61 | return fee; 62 | } 63 | 64 | public String getLabel() { 65 | return label; 66 | } 67 | 68 | public void setLabel(String label) { 69 | this.label = label; 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return hash.toString(); 75 | } 76 | 77 | @Override 78 | public boolean equals(Object o) { 79 | if (this == o) return true; 80 | if (o == null || getClass() != o.getClass()) return false; 81 | BlockTransactionHash that = (BlockTransactionHash) o; 82 | return hash.equals(that.hash) && height == that.height; 83 | } 84 | 85 | @Override 86 | public int hashCode() { 87 | return Objects.hash(hash, height); 88 | } 89 | 90 | public int compareTo(BlockTransactionHash reference) { 91 | if(height != reference.height) { 92 | return getComparisonHeight() - reference.getComparisonHeight(); 93 | } 94 | 95 | return hash.compareTo(reference.hash); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/BlockTransactionHashIndex.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.Sha256Hash; 4 | 5 | import java.util.Date; 6 | import java.util.Objects; 7 | 8 | public class BlockTransactionHashIndex extends BlockTransactionHash implements Comparable { 9 | private final long index; 10 | private final long value; 11 | private BlockTransactionHashIndex spentBy; 12 | private Status status; 13 | 14 | public BlockTransactionHashIndex(Sha256Hash hash, int height, Date date, Long fee, long index, long value) { 15 | this(hash, height, date, fee, index, value, null); 16 | } 17 | 18 | public BlockTransactionHashIndex(Sha256Hash hash, int height, Date date, Long fee, long index, long value, BlockTransactionHashIndex spentBy) { 19 | this(hash, height, date, fee, index, value, spentBy, null); 20 | } 21 | 22 | public BlockTransactionHashIndex(Sha256Hash hash, int height, Date date, Long fee, long index, long value, BlockTransactionHashIndex spentBy, String label) { 23 | super(hash, height, date, fee, label); 24 | this.index = index; 25 | this.value = value; 26 | this.spentBy = spentBy; 27 | } 28 | 29 | public long getIndex() { 30 | return index; 31 | } 32 | 33 | public long getValue() { 34 | return value; 35 | } 36 | 37 | public boolean isSpent() { 38 | return spentBy != null; 39 | } 40 | 41 | public BlockTransactionHashIndex getSpentBy() { 42 | return spentBy; 43 | } 44 | 45 | public void setSpentBy(BlockTransactionHashIndex spentBy) { 46 | this.spentBy = spentBy; 47 | } 48 | 49 | public Status getStatus() { 50 | return status; 51 | } 52 | 53 | public void setStatus(Status status) { 54 | this.status = status; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return getHash().toString() + ":" + index; 60 | } 61 | 62 | @Override 63 | public boolean equals(Object o) { 64 | if (this == o) return true; 65 | if (o == null || getClass() != o.getClass()) return false; 66 | if (!super.equals(o)) return false; 67 | BlockTransactionHashIndex that = (BlockTransactionHashIndex) o; 68 | return index == that.index && 69 | value == that.value && 70 | Objects.equals(spentBy, that.spentBy); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | return Objects.hash(super.hashCode(), index, value, spentBy); 76 | } 77 | 78 | @Override 79 | public int compareTo(BlockTransactionHashIndex reference) { 80 | int diff = super.compareTo(reference); 81 | if(diff != 0) { 82 | return diff; 83 | } 84 | 85 | diff = (int)(index - reference.index); 86 | if(diff != 0) { 87 | return diff; 88 | } 89 | 90 | diff = (int)(value - reference.value); 91 | if(diff != 0) { 92 | return diff; 93 | } 94 | 95 | return spentBy == null ? (reference.spentBy == null ? 0 : Integer.MIN_VALUE) : (reference.spentBy == null ? Integer.MAX_VALUE : spentBy.compareTo(reference.spentBy)); 96 | } 97 | 98 | public BlockTransactionHashIndex copy() { 99 | BlockTransactionHashIndex copy = new BlockTransactionHashIndex(super.getHash(), super.getHeight(), super.getDate(), super.getFee(), index, value, spentBy == null ? null : spentBy.copy(), super.getLabel()); 100 | copy.setId(getId()); 101 | return copy; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/CoinbaseTxoFilter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.Transaction; 4 | 5 | public class CoinbaseTxoFilter implements TxoFilter { 6 | private final Wallet wallet; 7 | 8 | public CoinbaseTxoFilter(Wallet wallet) { 9 | this.wallet = wallet; 10 | } 11 | 12 | @Override 13 | public boolean isEligible(BlockTransactionHashIndex candidate) { 14 | //Disallow immature coinbase outputs 15 | BlockTransaction blockTransaction = wallet.getWalletTransaction(candidate.getHash()); 16 | if(blockTransaction != null && blockTransaction.getTransaction() != null && blockTransaction.getTransaction().isCoinBase() 17 | && wallet.getStoredBlockHeight() != null && candidate.getConfirmations(wallet.getStoredBlockHeight()) < Transaction.COINBASE_MATURITY_THRESHOLD) { 18 | return false; 19 | } 20 | 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/ElectrumMnemonicCode.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.Utils; 4 | import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.text.Normalizer; 8 | import java.util.List; 9 | 10 | public class ElectrumMnemonicCode { 11 | private static final int PBKDF2_ROUNDS = 2048; 12 | 13 | private final List VALID_PREFIXES = List.of("01", "100", "101"); 14 | 15 | public static ElectrumMnemonicCode INSTANCE = new ElectrumMnemonicCode(); 16 | 17 | /** 18 | * Gets the word list this code uses. 19 | */ 20 | public List getWordList() { 21 | return Bip39MnemonicCode.INSTANCE.getWordList(); 22 | } 23 | 24 | public static byte[] toSeed(List seedWords, String passphrase) { 25 | String mnemonicWords = String.join(" ", seedWords); 26 | return toSeed(mnemonicWords, passphrase); 27 | } 28 | 29 | public static byte[] toSeed(String mnemonicWords, String passphrase) { 30 | if(passphrase == null) { 31 | passphrase = ""; 32 | } 33 | 34 | String mnemonic = Normalizer.normalize(mnemonicWords, Normalizer.Form.NFKD); 35 | String salt = "electrum" + Normalizer.normalize(passphrase, Normalizer.Form.NFKD); 36 | 37 | Pbkdf2KeyDeriver keyDeriver = new Pbkdf2KeyDeriver(salt.getBytes(StandardCharsets.UTF_8), PBKDF2_ROUNDS); 38 | return keyDeriver.deriveKey(mnemonic).getKeyBytes(); 39 | } 40 | 41 | /** 42 | * Check to see if a mnemonic word list is valid. 43 | */ 44 | public void check(List words) throws MnemonicException { 45 | String prefix = getPrefix(words); 46 | if(!VALID_PREFIXES.contains(prefix)) { 47 | throw new MnemonicException("Invalid prefix " + prefix); 48 | } 49 | } 50 | 51 | private String getPrefix(List words) throws MnemonicException { 52 | String mnemonic = String.join(" ", words); 53 | mnemonic = Normalizer.normalize(mnemonic, Normalizer.Form.NFKD); 54 | byte [] hash = Utils.getHmacSha512Hash("Seed version".getBytes(StandardCharsets.UTF_8), mnemonic.getBytes(StandardCharsets.UTF_8)); 55 | String hex = Utils.bytesToHex(hash); 56 | try { 57 | int prefixLength = Integer.parseInt(hex.substring(0, 1)) + 2; 58 | String prefix = hex.substring(0, prefixLength); 59 | return Integer.toHexString(Integer.parseInt(prefix, 16)); 60 | } catch(NumberFormatException e) { 61 | throw new MnemonicException("Invalid prefix bytes"); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/ExcludeTxoFilter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | 6 | public class ExcludeTxoFilter implements TxoFilter { 7 | private final Collection excludedTxos; 8 | 9 | public ExcludeTxoFilter() { 10 | this.excludedTxos = new ArrayList<>(); 11 | } 12 | 13 | public ExcludeTxoFilter(Collection excludedTxos) { 14 | this.excludedTxos = new ArrayList<>(excludedTxos); 15 | } 16 | 17 | @Override 18 | public boolean isEligible(BlockTransactionHashIndex candidate) { 19 | for(BlockTransactionHashIndex excludedTxo : excludedTxos) { 20 | if(candidate.getHash().equals(excludedTxo.getHash()) && candidate.getIndex() == excludedTxo.getIndex()) { 21 | return false; 22 | } 23 | } 24 | 25 | return true; 26 | } 27 | 28 | public Collection getExcludedTxos() { 29 | return excludedTxos; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/FrozenTxoFilter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class FrozenTxoFilter implements TxoFilter { 4 | @Override 5 | public boolean isEligible(BlockTransactionHashIndex candidate) { 6 | return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/InsufficientFundsException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class InsufficientFundsException extends Exception { 4 | private Long targetValue; 5 | 6 | public InsufficientFundsException() { 7 | super(); 8 | } 9 | 10 | public InsufficientFundsException(String msg) { 11 | super(msg); 12 | } 13 | 14 | public InsufficientFundsException(String message, Long targetValue) { 15 | super(message); 16 | this.targetValue = targetValue; 17 | } 18 | 19 | public Long getTargetValue() { 20 | return targetValue; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/InvalidKeystoreException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class InvalidKeystoreException extends Exception { 4 | public InvalidKeystoreException() { 5 | super(); 6 | } 7 | 8 | public InvalidKeystoreException(String msg) { 9 | super(msg); 10 | } 11 | 12 | public InvalidKeystoreException(String msg, Throwable throwable) { 13 | super(msg, throwable); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/InvalidWalletException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class InvalidWalletException extends Exception { 4 | public InvalidWalletException() { 5 | super(); 6 | } 7 | 8 | public InvalidWalletException(String msg) { 9 | super(msg); 10 | } 11 | 12 | public InvalidWalletException(String msg, Throwable throwable) { 13 | super(msg, throwable); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/KeystoreSource.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public enum KeystoreSource { 4 | HW_USB("Connected Hardware Wallet"), 5 | HW_AIRGAPPED("Airgapped Hardware Wallet"), 6 | SW_SEED("Software Wallet"), 7 | SW_WATCH("Watch Only Wallet"), 8 | SW_PAYMENT_CODE("Payment Code Wallet"); 9 | 10 | private final String displayName; 11 | 12 | KeystoreSource(String displayName) { 13 | this.displayName = displayName; 14 | } 15 | 16 | public String getDisplayName() { 17 | return displayName; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/MaxUtxoSelector.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import java.util.Collection; 4 | import java.util.stream.Collectors; 5 | 6 | public class MaxUtxoSelector extends SingleSetUtxoSelector { 7 | @Override 8 | public Collection select(long targetValue, Collection candidates) { 9 | return candidates.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).flatMap(outputGroup -> outputGroup.getUtxos().stream()).collect(Collectors.toUnmodifiableList()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/MixConfig.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import java.io.File; 4 | 5 | public class MixConfig extends Persistable { 6 | private String scode; 7 | private Boolean mixOnStartup; 8 | private String indexRange; 9 | private File mixToWalletFile; 10 | private String mixToWalletName; 11 | private Integer minMixes; 12 | private int receiveIndex; 13 | private int changeIndex; 14 | 15 | public MixConfig() { 16 | } 17 | 18 | public MixConfig(String scode, Boolean mixOnStartup, String indexRange, File mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex) { 19 | this.scode = scode; 20 | this.mixOnStartup = mixOnStartup; 21 | this.indexRange = indexRange; 22 | this.mixToWalletFile = mixToWalletFile; 23 | this.mixToWalletName = mixToWalletName; 24 | this.minMixes = minMixes; 25 | this.receiveIndex = receiveIndex; 26 | this.changeIndex = changeIndex; 27 | } 28 | 29 | public String getScode() { 30 | return scode; 31 | } 32 | 33 | public void setScode(String scode) { 34 | this.scode = scode; 35 | } 36 | 37 | public Boolean getMixOnStartup() { 38 | return mixOnStartup; 39 | } 40 | 41 | public void setMixOnStartup(Boolean mixOnStartup) { 42 | this.mixOnStartup = mixOnStartup; 43 | } 44 | 45 | public String getIndexRange() { 46 | return indexRange; 47 | } 48 | 49 | public void setIndexRange(String indexRange) { 50 | this.indexRange = indexRange; 51 | } 52 | 53 | public File getMixToWalletFile() { 54 | return mixToWalletFile; 55 | } 56 | 57 | public void setMixToWalletFile(File mixToWalletFile) { 58 | this.mixToWalletFile = mixToWalletFile; 59 | } 60 | 61 | public String getMixToWalletName() { 62 | return mixToWalletName; 63 | } 64 | 65 | public void setMixToWalletName(String mixToWalletName) { 66 | this.mixToWalletName = mixToWalletName; 67 | } 68 | 69 | public Integer getMinMixes() { 70 | return minMixes; 71 | } 72 | 73 | public void setMinMixes(Integer minMixes) { 74 | this.minMixes = minMixes; 75 | } 76 | 77 | public int getReceiveIndex() { 78 | return receiveIndex; 79 | } 80 | 81 | public void setReceiveIndex(int receiveIndex) { 82 | this.receiveIndex = receiveIndex; 83 | } 84 | 85 | public int getChangeIndex() { 86 | return changeIndex; 87 | } 88 | 89 | public void setChangeIndex(int changeIndex) { 90 | this.changeIndex = changeIndex; 91 | } 92 | 93 | public MixConfig copy() { 94 | return new MixConfig(scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/MnemonicException.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class MnemonicException extends Exception { 4 | private final String title; 5 | 6 | public MnemonicException() { 7 | super(); 8 | this.title = null; 9 | } 10 | 11 | public MnemonicException(String message) { 12 | this(message, message); 13 | } 14 | 15 | public MnemonicException(String title, String message) { 16 | super(message); 17 | this.title = title; 18 | } 19 | 20 | public String getTitle() { 21 | return title; 22 | } 23 | 24 | /** 25 | * Thrown when an argument to MnemonicCode is the wrong length. 26 | */ 27 | public static class MnemonicLengthException extends MnemonicException { 28 | public MnemonicLengthException(String message) { 29 | super(message); 30 | } 31 | 32 | public MnemonicLengthException(String title, String message) { 33 | super(title, message); 34 | } 35 | } 36 | 37 | /** 38 | * Thrown when a list of MnemonicCode words fails the checksum check. 39 | */ 40 | public static class MnemonicChecksumException extends MnemonicException { 41 | public MnemonicChecksumException() { 42 | super(); 43 | } 44 | 45 | public MnemonicChecksumException(String title, String message) { 46 | super(title, message); 47 | } 48 | } 49 | 50 | /** 51 | * Thrown when a word is encountered which is not in the MnemonicCode's word list. 52 | */ 53 | public static class MnemonicWordException extends MnemonicException { 54 | /** Contains the word that was not found in the word list. */ 55 | public final String badWord; 56 | 57 | public MnemonicWordException(String badWord) { 58 | super(); 59 | this.badWord = badWord; 60 | } 61 | } 62 | 63 | /** 64 | * Thrown when the mnemonic is valid, but for for the expected standard 65 | */ 66 | public static class MnemonicTypeException extends MnemonicException { 67 | public final DeterministicSeed.Type invalidType; 68 | 69 | public MnemonicTypeException(DeterministicSeed.Type invalidType) { 70 | super(); 71 | this.invalidType = invalidType; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/OutputGroup.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.ScriptType; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR; 9 | 10 | public class OutputGroup { 11 | private final List utxos = new ArrayList<>(); 12 | private final ScriptType scriptType; 13 | private final int walletBlockHeight; 14 | private final long inputWeightUnits; 15 | private final double feeRate; 16 | private final double longTermFeeRate; 17 | private long value = 0; 18 | private long effectiveValue = 0; 19 | private long fee = 0; 20 | private long longTermFee = 0; 21 | private int depth = Integer.MAX_VALUE; 22 | private boolean allInputsFromWallet = true; 23 | private boolean spendLast; 24 | 25 | public OutputGroup(ScriptType scriptType, int walletBlockHeight, long inputWeightUnits, double feeRate, double longTermFeeRate) { 26 | this.scriptType = scriptType; 27 | this.walletBlockHeight = walletBlockHeight; 28 | this.inputWeightUnits = inputWeightUnits; 29 | this.feeRate = feeRate; 30 | this.longTermFeeRate = longTermFeeRate; 31 | } 32 | 33 | public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet, boolean spendLast) { 34 | utxos.add(utxo); 35 | value += utxo.getValue(); 36 | effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR); 37 | fee += (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR); 38 | longTermFee += (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR); 39 | depth = utxo.getHeight() <= 0 ? 0 : Math.min(depth, walletBlockHeight - utxo.getHeight() + 1); 40 | this.allInputsFromWallet &= allInputsFromWallet; 41 | this.spendLast |= spendLast; 42 | } 43 | 44 | public void remove(BlockTransactionHashIndex utxo) { 45 | if(utxos.remove(utxo)) { 46 | value -= utxo.getValue(); 47 | effectiveValue -= (utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR)); 48 | fee -= (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR); 49 | longTermFee -= (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR); 50 | } 51 | } 52 | 53 | public List getUtxos() { 54 | return utxos; 55 | } 56 | 57 | public ScriptType getScriptType() { 58 | return scriptType; 59 | } 60 | 61 | public long getValue() { 62 | return value; 63 | } 64 | 65 | public long getEffectiveValue() { 66 | return effectiveValue; 67 | } 68 | 69 | public long getFee() { 70 | return fee; 71 | } 72 | 73 | public long getLongTermFee() { 74 | return longTermFee; 75 | } 76 | 77 | public int getDepth() { 78 | return depth; 79 | } 80 | 81 | public boolean isAllInputsFromWallet() { 82 | return allInputsFromWallet; 83 | } 84 | 85 | public boolean isSpendLast() { 86 | return spendLast; 87 | } 88 | 89 | public static class Filter { 90 | private final int minWalletConfirmations; 91 | private final int minExternalConfirmations; 92 | private final boolean includeSpendLast; 93 | 94 | public Filter(int minWalletConfirmations, int minExternalConfirmations, boolean includeSpendLast) { 95 | this.minWalletConfirmations = minWalletConfirmations; 96 | this.minExternalConfirmations = minExternalConfirmations; 97 | this.includeSpendLast = includeSpendLast; 98 | } 99 | 100 | public boolean isEligible(OutputGroup outputGroup) { 101 | if(outputGroup.isAllInputsFromWallet()) { 102 | return outputGroup.getDepth() >= minWalletConfirmations && (includeSpendLast || !outputGroup.isSpendLast()); 103 | } 104 | 105 | return outputGroup.getDepth() >= minExternalConfirmations && (includeSpendLast || !outputGroup.isSpendLast()); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/Payment.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.address.Address; 4 | import com.sparrowwallet.drongo.address.P2AAddress; 5 | 6 | public class Payment { 7 | private Address address; 8 | private String label; 9 | private long amount; 10 | private boolean sendMax; 11 | private Type type; 12 | 13 | public Payment(Address address, String label, long amount, boolean sendMax) { 14 | this(address, label, amount, sendMax, Type.DEFAULT); 15 | } 16 | 17 | public Payment(Address address, String label, long amount, boolean sendMax, Type type) { 18 | this.address = address; 19 | this.label = label == null && address instanceof P2AAddress ? address.getOutputScriptDataType() : label; 20 | this.amount = amount; 21 | this.sendMax = sendMax; 22 | this.type = type == Type.DEFAULT && address instanceof P2AAddress ? Type.ANCHOR : type; 23 | } 24 | 25 | public Address getAddress() { 26 | return address; 27 | } 28 | 29 | public void setAddress(Address address) { 30 | this.address = address; 31 | } 32 | 33 | public String getLabel() { 34 | return label; 35 | } 36 | 37 | public void setLabel(String label) { 38 | this.label = label; 39 | } 40 | 41 | public long getAmount() { 42 | return amount; 43 | } 44 | 45 | public void setAmount(long amount) { 46 | this.amount = amount; 47 | } 48 | 49 | public boolean isSendMax() { 50 | return sendMax; 51 | } 52 | 53 | public void setSendMax(boolean sendMax) { 54 | this.sendMax = sendMax; 55 | } 56 | 57 | public Type getType() { 58 | return type; 59 | } 60 | 61 | public void setType(Type type) { 62 | this.type = type; 63 | } 64 | 65 | public enum Type { 66 | DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX, ANCHOR; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/Persistable.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class Persistable { 4 | public static final int MAX_LABEL_LENGTH = 255; 5 | 6 | private Long id; 7 | 8 | public Long getId() { 9 | return id; 10 | } 11 | 12 | public void setId(Long id) { 13 | this.id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/PresetUtxoSelector.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import java.util.*; 4 | import java.util.stream.Collectors; 5 | 6 | public class PresetUtxoSelector extends SingleSetUtxoSelector { 7 | private final Collection presetUtxos; 8 | private final Collection excludedUtxos; 9 | private final boolean maintainOrder; 10 | private final boolean requireAll; 11 | 12 | public PresetUtxoSelector(Collection presetUtxos) { 13 | this(presetUtxos, new ArrayList<>()); 14 | } 15 | 16 | public PresetUtxoSelector(Collection presetUtxos, Collection excludedUtxos) { 17 | this.presetUtxos = presetUtxos; 18 | this.excludedUtxos = excludedUtxos; 19 | this.maintainOrder = false; 20 | this.requireAll = false; 21 | } 22 | 23 | public PresetUtxoSelector(Collection presetUtxos, boolean maintainOrder, boolean requireAll) { 24 | this.presetUtxos = presetUtxos; 25 | this.excludedUtxos = new ArrayList<>(); 26 | this.maintainOrder = maintainOrder; 27 | this.requireAll = requireAll; 28 | } 29 | 30 | @Override 31 | public Collection select(long targetValue, Collection candidates) { 32 | List flattenedCandidates = candidates.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).collect(Collectors.toList()); 33 | List utxos = new ArrayList<>(); 34 | 35 | //Don't use equals() here as we don't want to consider height which may change as txes are confirmed 36 | for(BlockTransactionHashIndex candidate : flattenedCandidates) { 37 | for(BlockTransactionHashIndex presetUtxo : presetUtxos) { 38 | if(candidate.getHash().equals(presetUtxo.getHash()) && candidate.getIndex() == presetUtxo.getIndex()) { 39 | utxos.add(candidate); 40 | } 41 | } 42 | } 43 | 44 | Set utxosSet = new HashSet<>(utxos); 45 | if(maintainOrder && utxosSet.containsAll(presetUtxos)) { 46 | return presetUtxos; 47 | } else if(requireAll && !utxosSet.containsAll(presetUtxos)) { 48 | return Collections.emptyList(); 49 | } 50 | 51 | return utxos; 52 | } 53 | 54 | public Collection getPresetUtxos() { 55 | return presetUtxos; 56 | } 57 | 58 | public Collection getExcludedUtxos() { 59 | return excludedUtxos; 60 | } 61 | 62 | public TxoFilter asExcludeTxoFilter() { 63 | List utxos = new ArrayList<>(); 64 | utxos.addAll(presetUtxos); 65 | utxos.addAll(excludedUtxos); 66 | return new ExcludeTxoFilter(utxos); 67 | } 68 | 69 | @Override 70 | public boolean shuffleInputs() { 71 | return !maintainOrder; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/PriorityUtxoSelector.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import java.math.BigInteger; 4 | import java.util.*; 5 | import java.util.stream.Collectors; 6 | 7 | public class PriorityUtxoSelector extends SingleSetUtxoSelector { 8 | private final int currentBlockHeight; 9 | 10 | public PriorityUtxoSelector(int currentBlockHeight) { 11 | this.currentBlockHeight = currentBlockHeight; 12 | } 13 | 14 | @Override 15 | public Collection select(long targetValue, Collection candidates) { 16 | List selected = new ArrayList<>(); 17 | 18 | List sorted = candidates.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).filter(ref -> ref.getHeight() > 0).collect(Collectors.toList()); 19 | sort(sorted); 20 | 21 | //Testing only: remove 22 | Collections.reverse(sorted); 23 | 24 | long total = 0; 25 | for(BlockTransactionHashIndex reference : sorted) { 26 | if(total > targetValue) { 27 | break; 28 | } 29 | 30 | selected.add(reference); 31 | total += reference.getValue(); 32 | } 33 | 34 | return selected; 35 | } 36 | 37 | private void sort(List outputs) { 38 | outputs.sort((a, b) -> { 39 | int depthA = currentBlockHeight - a.getHeight(); 40 | int depthB = currentBlockHeight - b.getHeight(); 41 | 42 | Long valueA = a.getValue(); 43 | Long valueB = b.getValue(); 44 | 45 | BigInteger coinDepthA = BigInteger.valueOf(depthA).multiply(BigInteger.valueOf(valueA)); 46 | BigInteger coinDepthB = BigInteger.valueOf(depthB).multiply(BigInteger.valueOf(valueB)); 47 | 48 | int coinDepthCompare = coinDepthB.compareTo(coinDepthA); 49 | if (coinDepthCompare != 0) { 50 | return coinDepthCompare; 51 | } 52 | 53 | // The "coin*days" destroyed are equal, sort by value alone to get the lowest transaction size. 54 | int coinValueCompare = valueB.compareTo(valueA); 55 | if (coinValueCompare != 0) { 56 | return coinValueCompare; 57 | } 58 | 59 | return a.compareTo(b); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/SeedQR.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.Utils; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.IntStream; 9 | 10 | public class SeedQR { 11 | public static DeterministicSeed getSeed(String seedQr) { 12 | if(seedQr.length() < 48 || seedQr.length() > 96 || seedQr.length() % 4 > 0) { 13 | throw new IllegalArgumentException("Invalid SeedQR length: " + seedQr.length()); 14 | } 15 | 16 | if(!seedQr.chars().allMatch(c -> c >= '0' && c <= '9')) { 17 | throw new IllegalArgumentException("SeedQR contains non-digit characters: " + seedQr); 18 | } 19 | 20 | List indexes = IntStream.iterate(0, i -> i + 4).limit((int)Math.ceil(seedQr.length() / 4.0)) 21 | .mapToObj(i -> seedQr.substring(i, Math.min(i + 4, seedQr.length()))) 22 | .map(Integer::parseInt) 23 | .collect(Collectors.toList()); 24 | 25 | List words = new ArrayList<>(indexes.size()); 26 | for(Integer index : indexes) { 27 | words.add(Bip39MnemonicCode.INSTANCE.getWordList().get(index)); 28 | } 29 | 30 | return new DeterministicSeed(words, null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39); 31 | } 32 | 33 | public static String getSeedQR(DeterministicSeed seed) { 34 | if(seed.isEncrypted()) { 35 | throw new IllegalStateException("Seed cannot be encrypted"); 36 | } 37 | 38 | StringBuilder builder = new StringBuilder(); 39 | for(String word : seed.getMnemonicCode()) { 40 | int index = Bip39MnemonicCode.INSTANCE.getWordList().indexOf(word); 41 | builder.append(String.format("%04d", index)); 42 | } 43 | 44 | return builder.toString(); 45 | } 46 | 47 | public static DeterministicSeed getSeed(byte[] compactSeedQr) { 48 | byte[] seed; 49 | 50 | if(compactSeedQr.length == 16 || compactSeedQr.length == 32) { 51 | //Assume scan contains seed only 52 | seed = compactSeedQr; 53 | } else { 54 | //Assume scan contains header, seed and EC bytes 55 | if(compactSeedQr[0] != 0x41 && compactSeedQr[0] != 0x42) { 56 | throw new IllegalArgumentException("Invalid CompactSeedQR header"); 57 | } 58 | 59 | if(compactSeedQr.length < 19) { 60 | throw new IllegalArgumentException("Invalid CompactSeedQR length"); 61 | } 62 | 63 | String qrHex = Utils.bytesToHex(compactSeedQr); 64 | String seedHex; 65 | if(qrHex.endsWith("0ec11ec11")) { 66 | seedHex = qrHex.substring(3, qrHex.length() - 9); //12 word, high EC 67 | } else if(qrHex.endsWith("0ec")) { 68 | seedHex = qrHex.substring(3, qrHex.length() - 3); //12 word, low EC 69 | } else { 70 | seedHex = qrHex.substring(3, qrHex.length() - 1); //24 word 71 | } 72 | 73 | seed = Utils.hexToBytes(seedHex); 74 | } 75 | 76 | if(seed.length < 16 || seed.length > 32 || seed.length % 4 > 0) { 77 | throw new IllegalArgumentException("Invalid CompactSeedQR length: " + compactSeedQr.length); 78 | } 79 | 80 | return new DeterministicSeed(seed, null, System.currentTimeMillis()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/SingleSetUtxoSelector.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | 6 | public abstract class SingleSetUtxoSelector implements UtxoSelector { 7 | @Override 8 | public List> selectSets(long targetValue, Collection candidates) { 9 | return List.of(select(targetValue, candidates)); 10 | } 11 | 12 | public abstract Collection select(long targetValue, Collection candidates); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/SortDirection.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public enum SortDirection { 4 | ASCENDING, DESCENDING 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/SpentTxoFilter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.Sha256Hash; 4 | 5 | public class SpentTxoFilter implements TxoFilter { 6 | private final Sha256Hash replacedTxid; 7 | 8 | public SpentTxoFilter() { 9 | replacedTxid = null; 10 | } 11 | 12 | public SpentTxoFilter(Sha256Hash replacedTxid) { 13 | this.replacedTxid = replacedTxid; 14 | } 15 | 16 | @Override 17 | public boolean isEligible(BlockTransactionHashIndex candidate) { 18 | return !isSpentOrReplaced(candidate); 19 | } 20 | 21 | private boolean isSpentOrReplaced(BlockTransactionHashIndex candidate) { 22 | return candidate.getHash().equals(replacedTxid) || (candidate.isSpent() && !candidate.getSpentBy().getHash().equals(replacedTxid)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/Status.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public enum Status { 4 | FROZEN 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/StonewallUtxoSelector.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.ScriptType; 4 | 5 | import java.util.*; 6 | import java.util.stream.Collectors; 7 | 8 | public class StonewallUtxoSelector implements UtxoSelector { 9 | private final ScriptType preferredScriptType; 10 | private final long noInputsFee; 11 | 12 | //Use the same seed so the UTXO selection is deterministic 13 | private final Random random = new Random(42); 14 | 15 | public StonewallUtxoSelector(ScriptType preferredScriptType, long noInputsFee) { 16 | this.preferredScriptType = preferredScriptType; 17 | this.noInputsFee = noInputsFee; 18 | } 19 | 20 | @Override 21 | public List> selectSets(long targetValue, Collection candidates) { 22 | long actualTargetValue = targetValue + noInputsFee; 23 | 24 | List uniqueCandidates = new ArrayList<>(); 25 | for(OutputGroup candidate : candidates) { 26 | OutputGroup existingTxGroup = getTransactionAlreadySelected(uniqueCandidates, candidate); 27 | if(existingTxGroup != null) { 28 | if(candidate.getValue() > existingTxGroup.getValue()) { 29 | uniqueCandidates.remove(existingTxGroup); 30 | uniqueCandidates.add(candidate); 31 | } 32 | } else { 33 | uniqueCandidates.add(candidate); 34 | } 35 | } 36 | 37 | List preferredCandidates = uniqueCandidates.stream().filter(outputGroup -> outputGroup.getScriptType().equals(preferredScriptType)).collect(Collectors.toList()); 38 | List> preferredSets = selectSets(targetValue, preferredCandidates, actualTargetValue); 39 | if(!preferredSets.isEmpty()) { 40 | return preferredSets; 41 | } 42 | 43 | return selectSets(targetValue, uniqueCandidates, actualTargetValue); 44 | } 45 | 46 | private List> selectSets(long targetValue, List uniqueCandidates, long actualTargetValue) { 47 | for(int i = 0; i < 15; i++) { 48 | List randomized = new ArrayList<>(uniqueCandidates); 49 | Collections.shuffle(randomized, random); 50 | 51 | List set1 = new ArrayList<>(); 52 | long selectedValue1 = getUtxoSet(actualTargetValue, set1, randomized); 53 | 54 | List set2 = new ArrayList<>(); 55 | long selectedValue2 = getUtxoSet(actualTargetValue, set2, randomized); 56 | 57 | if(selectedValue1 >= targetValue && selectedValue2 >= targetValue) { 58 | return List.of(getUtxos(set1), getUtxos(set2)); 59 | } 60 | } 61 | 62 | return Collections.emptyList(); 63 | } 64 | 65 | private long getUtxoSet(long targetValue, List selectedSet, List randomized) { 66 | long selectedValue = 0; 67 | while(selectedValue <= targetValue && !randomized.isEmpty()) { 68 | OutputGroup candidate = randomized.remove(0); 69 | selectedSet.add(candidate); 70 | selectedValue = selectedSet.stream().mapToLong(OutputGroup::getEffectiveValue).sum(); 71 | } 72 | 73 | return selectedValue; 74 | } 75 | 76 | private OutputGroup getTransactionAlreadySelected(List selected, OutputGroup candidateGroup) { 77 | for(OutputGroup selectedGroup : selected) { 78 | for(BlockTransactionHashIndex selectedUtxo : selectedGroup.getUtxos()) { 79 | for(BlockTransactionHashIndex candidateUtxo : candidateGroup.getUtxos()) { 80 | if(selectedUtxo.getHash().equals(candidateUtxo.getHash())) { 81 | return selectedGroup; 82 | } 83 | } 84 | } 85 | } 86 | 87 | return null; 88 | } 89 | 90 | private Collection getUtxos(List set) { 91 | return set.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).collect(Collectors.toList()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/TableType.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public enum TableType { 4 | TRANSACTIONS, UTXOS, RECEIVE_ADDRESSES, CHANGE_ADDRESSES, SEARCH_WALLET, WALLET_SUMMARY 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/TxoFilter.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public interface TxoFilter { 4 | boolean isEligible(BlockTransactionHashIndex candidate); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/UtxoMixData.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class UtxoMixData extends Persistable { 4 | private final int mixesDone; 5 | private final Long expired; 6 | 7 | public UtxoMixData(int mixesDone, Long expired) { 8 | this.mixesDone = mixesDone; 9 | this.expired = expired; 10 | } 11 | 12 | public int getMixesDone() { 13 | return mixesDone; 14 | } 15 | 16 | public Long getExpired() { 17 | return expired; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "{mixesDone:" + mixesDone + ", expired: " + expired + "}"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/UtxoSelector.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | 6 | public interface UtxoSelector { 7 | List> selectSets(long targetValue, Collection candidates); 8 | default boolean shuffleInputs() { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/WalletConfig.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class WalletConfig extends Persistable { 4 | private byte[] iconData; 5 | private boolean userIcon; 6 | private boolean usePayNym; 7 | 8 | public WalletConfig() { 9 | } 10 | 11 | public WalletConfig(byte[] iconData, boolean userIcon, boolean usePayNym) { 12 | this.iconData = iconData; 13 | this.userIcon = userIcon; 14 | this.usePayNym = usePayNym; 15 | } 16 | 17 | public byte[] getIconData() { 18 | return iconData; 19 | } 20 | 21 | public boolean isUserIcon() { 22 | return userIcon; 23 | } 24 | 25 | public void setIconData(byte[] iconData, boolean userIcon) { 26 | this.iconData = iconData; 27 | this.userIcon = userIcon; 28 | } 29 | 30 | public boolean isUsePayNym() { 31 | return usePayNym; 32 | } 33 | 34 | public void setUsePayNym(boolean usePayNym) { 35 | this.usePayNym = usePayNym; 36 | } 37 | 38 | public WalletConfig copy() { 39 | return new WalletConfig(iconData, userIcon, usePayNym); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/WalletTable.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | public class WalletTable extends Persistable { 4 | private final TableType tableType; 5 | private final Double[] widths; 6 | private final int sortColumn; 7 | private final SortDirection sortDirection; 8 | 9 | public WalletTable(TableType tableType, Double[] widths, int sortColumn, SortDirection sortDirection) { 10 | this.tableType = tableType; 11 | this.widths = widths; 12 | this.sortColumn = sortColumn; 13 | this.sortDirection = sortDirection; 14 | } 15 | 16 | public WalletTable(TableType tableType, Double[] widths, Sort sort) { 17 | this.tableType = tableType; 18 | this.widths = widths; 19 | this.sortColumn = sort.sortColumn; 20 | this.sortDirection = sort.sortDirection; 21 | } 22 | 23 | public TableType getTableType() { 24 | return tableType; 25 | } 26 | 27 | public Double[] getWidths() { 28 | return widths; 29 | } 30 | 31 | public int getSortColumn() { 32 | return sortColumn; 33 | } 34 | 35 | public SortDirection getSortDirection() { 36 | return sortDirection; 37 | } 38 | 39 | public Sort getSort() { 40 | return new Sort(sortColumn, sortDirection); 41 | } 42 | 43 | @Override 44 | public final boolean equals(Object o) { 45 | if(this == o) { 46 | return true; 47 | } 48 | if(!(o instanceof WalletTable that)) { 49 | return false; 50 | } 51 | 52 | return tableType == that.tableType; 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return tableType.hashCode(); 58 | } 59 | 60 | public record Sort(int sortColumn, SortDirection sortDirection) {} 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/slip39/Cipher.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet.slip39; 2 | 3 | import javax.crypto.SecretKeyFactory; 4 | import javax.crypto.spec.PBEKeySpec; 5 | import java.nio.charset.StandardCharsets; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.security.spec.InvalidKeySpecException; 8 | import java.util.Arrays; 9 | 10 | import static com.sparrowwallet.drongo.wallet.slip39.Share.*; 11 | import static com.sparrowwallet.drongo.wallet.slip39.Utils.concatenate; 12 | 13 | public class Cipher { 14 | public static byte[] xor(byte[] a, byte[] b) { 15 | byte[] result = new byte[a.length]; 16 | for(int i = 0; i < a.length; i++) { 17 | result[i] = (byte) (a[i] ^ b[i]); 18 | } 19 | return result; 20 | } 21 | 22 | public static byte[] roundFunction(int i, byte[] passphrase, int e, byte[] salt, byte[] r) { 23 | int iterations = (BASE_ITERATION_COUNT << e) / ROUND_COUNT; 24 | byte[] input = new byte[1 + passphrase.length]; 25 | input[0] = (byte) i; 26 | System.arraycopy(passphrase, 0, input, 1, passphrase.length); 27 | 28 | try { 29 | PBEKeySpec spec = new PBEKeySpec(new String(input, StandardCharsets.UTF_8).toCharArray(), concatenate(salt, r), iterations, r.length * 8); 30 | SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); 31 | return skf.generateSecret(spec).getEncoded(); 32 | } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { 33 | throw new RuntimeException("Error during round function", ex); 34 | } 35 | } 36 | 37 | public static byte[] getSalt(int identifier, boolean extendable) { 38 | if(extendable) { 39 | return new byte[0]; 40 | } 41 | int identifierLen = Utils.bitsToBytes(ID_LENGTH_BITS); 42 | byte[] idBytes = toByteArray(identifier, identifierLen); 43 | return concatenate(CUSTOMIZATION_STRING_ORIG, idBytes); 44 | } 45 | 46 | public static byte[] encrypt(byte[] masterSecret, byte[] passphrase, int iterationExponent, int identifier, boolean extendable) { 47 | if (masterSecret.length % 2 != 0) { 48 | throw new IllegalArgumentException("The length of the master secret in bytes must be an even number."); 49 | } 50 | 51 | byte[] l = Arrays.copyOfRange(masterSecret, 0, masterSecret.length / 2); 52 | byte[] r = Arrays.copyOfRange(masterSecret, masterSecret.length / 2, masterSecret.length); 53 | byte[] salt = getSalt(identifier, extendable); 54 | 55 | for (int i = 0; i < ROUND_COUNT; i++) { 56 | byte[] f = roundFunction(i, passphrase, iterationExponent, salt, r); 57 | byte[] newR = xor(l, f); 58 | l = r; 59 | r = newR; 60 | } 61 | 62 | return concatenate(r, l); 63 | } 64 | 65 | public static byte[] decrypt(byte[] encryptedMasterSecret, byte[] passphrase, int iterationExponent, int identifier, boolean extendable) { 66 | if (encryptedMasterSecret.length % 2 != 0) { 67 | throw new IllegalArgumentException("The length of the encrypted master secret in bytes must be an even number."); 68 | } 69 | 70 | byte[] l = Arrays.copyOfRange(encryptedMasterSecret, 0, encryptedMasterSecret.length / 2); 71 | byte[] r = Arrays.copyOfRange(encryptedMasterSecret, encryptedMasterSecret.length / 2, encryptedMasterSecret.length); 72 | byte[] salt = getSalt(identifier, extendable); 73 | 74 | for (int i = ROUND_COUNT - 1; i >= 0; i--) { 75 | byte[] f = roundFunction(i, passphrase, iterationExponent, salt, r); 76 | byte[] newR = xor(l, f); 77 | l = r; 78 | r = newR; 79 | } 80 | 81 | return concatenate(r, l); 82 | } 83 | 84 | private static byte[] toByteArray(int value, int length) { 85 | byte[] result = new byte[length]; 86 | for (int i = length - 1; i >= 0; i--) { 87 | result[i] = (byte) (value & 0xFF); 88 | value >>= 8; 89 | } 90 | return result; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/slip39/EncryptedMasterSecret.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet.slip39; 2 | 3 | import java.util.Arrays; 4 | 5 | public class EncryptedMasterSecret { 6 | private final int identifier; 7 | private final boolean extendable; 8 | private final int iterationExponent; 9 | private final byte[] ciphertext; 10 | 11 | public EncryptedMasterSecret(int identifier, boolean extendable, int iterationExponent, byte[] ciphertext) { 12 | this.identifier = identifier; 13 | this.extendable = extendable; 14 | this.iterationExponent = iterationExponent; 15 | this.ciphertext = Arrays.copyOf(ciphertext, ciphertext.length); 16 | } 17 | 18 | public int getIdentifier() { 19 | return identifier; 20 | } 21 | 22 | public boolean isExtendable() { 23 | return extendable; 24 | } 25 | 26 | public int getIterationExponent() { 27 | return iterationExponent; 28 | } 29 | 30 | public byte[] getCiphertext() { 31 | return Arrays.copyOf(ciphertext, ciphertext.length); 32 | } 33 | 34 | public static EncryptedMasterSecret fromMasterSecret(byte[] masterSecret, byte[] passphrase, int identifier, boolean extendable, int iterationExponent) { 35 | byte[] ciphertext = Cipher.encrypt(masterSecret, passphrase, iterationExponent, identifier, extendable); 36 | return new EncryptedMasterSecret(identifier, extendable, iterationExponent, ciphertext); 37 | } 38 | 39 | public byte[] decrypt(byte[] passphrase) { 40 | return Cipher.decrypt(ciphertext, passphrase, iterationExponent, identifier, extendable); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/slip39/GroupParams.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet.slip39; 2 | 3 | public record GroupParams(int threshold, int size) {} 4 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/slip39/RawShare.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet.slip39; 2 | 3 | public record RawShare(int index, byte[] value) {} 4 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/slip39/Rs1024.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet.slip39; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static com.sparrowwallet.drongo.wallet.slip39.Share.CHECKSUM_LENGTH_WORDS; 7 | 8 | public class Rs1024 { 9 | private static final int[] GEN = { 10 | 0xE0E040, 11 | 0x1C1C080, 12 | 0x3838100, 13 | 0x7070200, 14 | 0xE0E0009, 15 | 0x1C0C2412, 16 | 0x38086C24, 17 | 0x3090FC48, 18 | 0x21B1F890, 19 | 0x3F3F120 20 | }; 21 | 22 | private static int polymod(List values) { 23 | int chk = 1; 24 | for(int v : values) { 25 | int b = chk >> 20; 26 | chk = (chk & 0xFFFFF) << 10 ^ v; 27 | for(int i = 0; i < 10; i++) { 28 | if(((b >> i) & 1) != 0) { 29 | chk ^= GEN[i]; 30 | } 31 | } 32 | } 33 | return chk; 34 | } 35 | 36 | public static List createChecksum(List data, byte[] customizationString) { 37 | List values = new ArrayList<>(); 38 | for(byte b : customizationString) { 39 | values.add((int) b & 0xFF); 40 | } 41 | values.addAll(data); 42 | for(int i = 0; i < CHECKSUM_LENGTH_WORDS; i++) { 43 | values.add(0); 44 | } 45 | 46 | int polymod = polymod(values) ^ 1; 47 | List checksum = new ArrayList<>(CHECKSUM_LENGTH_WORDS); 48 | for(int i = CHECKSUM_LENGTH_WORDS - 1; i >= 0; i--) { 49 | checksum.add((polymod >> (10 * i)) & 1023); 50 | } 51 | return checksum; 52 | } 53 | 54 | public static boolean verifyChecksum(List data, byte[] customizationString) { 55 | List values = new ArrayList<>(); 56 | for(byte b : customizationString) { 57 | values.add((int) b & 0xFF); 58 | } 59 | values.addAll(data); 60 | 61 | return polymod(values) == 1; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/slip39/ShareGroup.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet.slip39; 2 | 3 | import com.sparrowwallet.drongo.wallet.MnemonicException; 4 | 5 | import java.util.*; 6 | 7 | public class ShareGroup { 8 | private final Set shares; 9 | 10 | public ShareGroup() { 11 | this.shares = new HashSet<>(); 12 | } 13 | 14 | public Iterator iterator() { 15 | return this.shares.iterator(); 16 | } 17 | 18 | public int size() { 19 | return this.shares.size(); 20 | } 21 | 22 | public boolean isEmpty() { 23 | return this.shares.isEmpty(); 24 | } 25 | 26 | public boolean contains(Share share) { 27 | return this.shares.contains(share); 28 | } 29 | 30 | public void add(Share share) throws MnemonicException { 31 | if(!this.shares.isEmpty() && !this.getGroupParameters().equals(share.getGroupParameters())) { 32 | throw new MnemonicException("Invalid mnemonic", "Invalid set of mnemonics, group parameters don't match."); 33 | } 34 | this.shares.add(share); 35 | } 36 | 37 | public List toRawShares() { 38 | List rawShares = new ArrayList<>(); 39 | for(Share s : this.shares) { 40 | rawShares.add(new RawShare(s.getIndex(), s.getValue())); 41 | } 42 | return rawShares; 43 | } 44 | 45 | public ShareGroup getMinimalGroup() { 46 | ShareGroup group = new ShareGroup(); 47 | int threshold = this.getMemberThreshold(); 48 | Iterator iterator = this.shares.iterator(); 49 | while(group.shares.size() < threshold && iterator.hasNext()) { 50 | group.shares.add(iterator.next()); 51 | } 52 | return group; 53 | } 54 | 55 | public Share.CommonParameters getCommonParameters() { 56 | return this.shares.iterator().next().getCommonParameters(); 57 | } 58 | 59 | public Share.GroupParameters getGroupParameters() { 60 | return this.shares.iterator().next().getGroupParameters(); 61 | } 62 | 63 | public int getMemberThreshold() { 64 | return this.shares.iterator().next().getMemberThreshold(); 65 | } 66 | 67 | public boolean isComplete() { 68 | return !this.shares.isEmpty() && this.shares.size() >= this.getMemberThreshold(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/sparrowwallet/drongo/wallet/slip39/Utils.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet.slip39; 2 | 3 | import java.math.BigInteger; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class Utils { 9 | public static final int RADIX_BITS = 10; 10 | 11 | public static int roundBits(int n, int radixBits) { 12 | // Get the number of `radixBits`-sized digits required to store a `n`-bit value. 13 | return (n + radixBits - 1) / radixBits; 14 | } 15 | 16 | public static int bitsToBytes(int n) { 17 | // Round up bit count to whole bytes. 18 | return roundBits(n, 8); 19 | } 20 | 21 | public static int bitsToWords(int n) { 22 | // Round up bit count to a multiple of word size. 23 | return roundBits(n, RADIX_BITS); 24 | } 25 | 26 | public static List intToIndices(BigInteger value, int length, int radixBits) { 27 | // Convert an integer value to indices in big endian order. 28 | BigInteger mask = BigInteger.ONE.shiftLeft(radixBits).subtract(BigInteger.ONE); 29 | List indices = new ArrayList<>(length); 30 | for(int i = length - 1; i >= 0; i--) { 31 | indices.add((value.shiftRight(i * radixBits)).and(mask).intValue()); 32 | } 33 | return indices; 34 | } 35 | 36 | public static byte[] concatenate(byte[]... arrays) { 37 | int totalLength = Arrays.stream(arrays).mapToInt(a -> a.length).sum(); 38 | byte[] result = new byte[totalLength]; 39 | int offset = 0; 40 | for(byte[] array : arrays) { 41 | System.arraycopy(array, 0, result, offset, array.length); 42 | offset += array.length; 43 | } 44 | return result; 45 | } 46 | 47 | public static int byteArrayToInt(byte[] bytes) { 48 | int value = 0; 49 | for(byte b : bytes) { 50 | value = (value << 8) | (b & 0xFF); 51 | } 52 | return value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | open module com.sparrowwallet.drongo { 2 | requires org.bouncycastle.provider; 3 | requires org.bouncycastle.pg; 4 | requires org.pgpainless.core; 5 | requires de.mkammerer.argon2.nolibs; 6 | requires org.slf4j; 7 | requires ch.qos.logback.core; 8 | requires ch.qos.logback.classic; 9 | exports com.sparrowwallet.drongo; 10 | exports com.sparrowwallet.drongo.psbt; 11 | exports com.sparrowwallet.drongo.protocol; 12 | exports com.sparrowwallet.drongo.address; 13 | exports com.sparrowwallet.drongo.crypto; 14 | exports com.sparrowwallet.drongo.wallet; 15 | exports com.sparrowwallet.drongo.pgp; 16 | exports com.sparrowwallet.drongo.policy; 17 | exports com.sparrowwallet.drongo.uri; 18 | exports com.sparrowwallet.drongo.bip47; 19 | exports com.sparrowwallet.drongo.wallet.slip39; 20 | exports org.bitcoin; 21 | } -------------------------------------------------------------------------------- /src/main/java/org/bitcoin/NativeSecp256k1Util.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2016 the libsecp256k1 contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.bitcoin; 18 | 19 | public class NativeSecp256k1Util{ 20 | 21 | public static void assertEquals( int val, int val2, String message ) throws AssertFailException{ 22 | if( val != val2 ) 23 | throw new AssertFailException("FAIL: " + message); 24 | } 25 | 26 | public static void assertEquals( boolean val, boolean val2, String message ) throws AssertFailException{ 27 | if( val != val2 ) 28 | throw new AssertFailException("FAIL: " + message); 29 | else 30 | System.out.println("PASS: " + message); 31 | } 32 | 33 | public static void assertEquals( String val, String val2, String message ) throws AssertFailException{ 34 | if( !val.equals(val2) ) 35 | throw new AssertFailException("FAIL: " + message); 36 | else 37 | System.out.println("PASS: " + message); 38 | } 39 | 40 | public static class AssertFailException extends Exception { 41 | public AssertFailException(String message) { 42 | super( message ); 43 | } 44 | } 45 | 46 | public static void checkArgument(boolean expression) { 47 | if (!expression) { 48 | throw new IllegalArgumentException(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/bitcoin/Secp256k1Context.java: -------------------------------------------------------------------------------- 1 | package org.bitcoin; 2 | 3 | import com.sparrowwallet.drongo.NativeUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.io.IOException; 8 | 9 | public class Secp256k1Context { 10 | 11 | private static final boolean enabled; // true if the library is loaded 12 | private static final long context; // ref to pointer to context obj 13 | 14 | private static final Logger log = LoggerFactory.getLogger(Secp256k1Context.class); 15 | 16 | static { // static initializer 17 | enabled = loadLibrary(); 18 | if(enabled) { 19 | context = secp256k1_init_context(); 20 | } else { 21 | context = -1; 22 | } 23 | } 24 | 25 | public static boolean isEnabled() { 26 | return enabled; 27 | } 28 | 29 | public static long getContext() { 30 | if (!enabled) 31 | return -1; // sanity check 32 | return context; 33 | } 34 | 35 | private static boolean loadLibrary() { 36 | try { 37 | String osName = System.getProperty("os.name"); 38 | String osArch = System.getProperty("os.arch"); 39 | if(osName.startsWith("Mac") && osArch.equals("aarch64")) { 40 | NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libsecp256k1.dylib"); 41 | } else if(osName.startsWith("Mac")) { 42 | NativeUtils.loadLibraryFromJar("/native/osx/x64/libsecp256k1.dylib"); 43 | } else if(osName.startsWith("Windows")) { 44 | NativeUtils.loadLibraryFromJar("/native/windows/x64/libsecp256k1-0.dll"); 45 | } else if(osArch.equals("aarch64")) { 46 | NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libsecp256k1.so"); 47 | } else { 48 | NativeUtils.loadLibraryFromJar("/native/linux/x64/libsecp256k1.so"); 49 | } 50 | 51 | return true; 52 | } catch(IOException e) { 53 | log.error("Error loading libsecp256k1 library", e); 54 | } 55 | 56 | return false; 57 | } 58 | 59 | private static native long secp256k1_init_context(); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/gpg/Calin_Culianu_(NilacTheGrim)_calin.culianu_gmail.com.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 37 | =PElt 38 | -----END PGP PUBLIC KEY BLOCK----- 39 | -------------------------------------------------------------------------------- /src/main/resources/gpg/Ken_Carpenter_(CTO__Foundation_Devices__Inc.)_ken_foundationdevices.com.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 51 | =/fv8 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /src/main/resources/gpg/SatoshiLabs_2021_Signing_Key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 39 | =mmx4 40 | -----END PGP PUBLIC KEY BLOCK----- 41 | -------------------------------------------------------------------------------- /src/main/resources/gpg/ShiftCrypto_Security_security_shiftcrypto.ch.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 50 | jLZugv6bMuMLjA== 51 | =0krM 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /src/main/resources/gpg/Stepan_Snigirev_(Specter_release_signing_key)_snigirev.stepan_gmail.com.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mE8EX3YN+BMFK4EEAAoCAwRhTEFptcVn6WAE4PbCTCeeHRmqQ5Yd49wCdycs9Sje 4 | 2B/rBPEGADac9PRmIXhjtWn3pifZ/sG2hFzl7G3VGIDttElTdGVwYW4gU25pZ2ly 5 | ZXYgKFNwZWN0ZXIgcmVsZWFzZSBzaWduaW5nIGtleSkgPHNuaWdpcmV2LnN0ZXBh 6 | bkBnbWFpbC5jb20+iJYEExMIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AW 7 | IQRvFuNU+DOT1uUuwl827TV6skuRXwUCY2bpqAUJDVbdMAAKCRA27TV6skuRX4CZ 8 | AP9Ad+uiNdSBelmn9B+/N0rWiH+xWL+nun5zDjHChKSm6gD/ei3znO5TH7x0fXuv 9 | OK61DwMTSU3GjoZH+fd4mn9ZczQ= 10 | =cUgx 11 | -----END PGP PUBLIC KEY BLOCK----- 12 | -------------------------------------------------------------------------------- /src/main/resources/gpg/T_Dev_D_(Samourai)_dev_samouraiwallet.com.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 49 | KacID3YioW9iTP4/YO7JZ3raYzg= 50 | =q8Nb 51 | -----END PGP PUBLIC KEY BLOCK----- 52 | -------------------------------------------------------------------------------- /src/main/resources/gpg/seedsigner_btc.hardware.solutions_gmail.com.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQGNBGCQlVIBDACdFq+XXr2IsvTHj4/uYiaFidkX1OpYZHaYyRpWr7qrTPfIiCzU 4 | qzPfdtPuzAIkaZ34MQNebEybQFq9y1DKx7npnVL+0Hl5X7tIxLgngz/pA+HVZ0ig 5 | IEA7iX2gutocj/ilCWC+23Nqt+n2ViAFFW7IZhBzY582303vsFTtoLtEDY0OP6B9 6 | HxFQbls4PrcTl+OORULEipfPB9ta6kfT7xu9jtnfKB9ZuPB+YTP+IzPmmOLTKV8I 7 | 88vuHHagVwZp1eZWFBvYJFFAic5IZhwUiReYhHfuTsBV7o/kn/A/ubwM30FvjoBL 8 | y4o2XUUF7eufmXrkRjkYgoFSkZhG38O+WtF/bOJZO8BQQworIlzQuVC1ZlH7Yq6r 9 | 7W5VBKiyWkhSZuBdwQMC0zJ7uWTuT6hU/aF+aca2unK6NrP0oI6REc+N3gFusYtE 10 | 1Ak3pJtab8kurHb3peFtabeWpsicmbnf1tCD6IMqUT+iyETZuQk0JzMb98ClDz5/ 11 | xXbmRr9+VfzRTckAEQEAAbQtc2VlZHNpZ25lciA8YnRjLmhhcmR3YXJlLnNvbHV0 12 | aW9uc0BnbWFpbC5jb20+iQHOBBMBCgA4FiEERnObdLVq2I8UsIgux+9wkAcmARkF 13 | AmCQlVICGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQx+9wkAcmARl9igv/ 14 | WgGIxc1I4L9ACLotU3hUhdFyGSxdtwvHHDn9nkOiTefLqBN9BtqiCTKuebHj7Qy5 15 | rQm27ZkRDIH0zGUxepMWEy8+n2bjF8BBu03CZvPBuLeRXj4avGROcMbNkTxnfeSD 16 | Gbw3/DmD67CoMSwlhzlXHC7Vazx2cFSyWsRi6y5jQt5whfGkf74MAhSRGM4iwAq2 17 | 1i+660eqJDNG6juSG7IVSL7gvxvsnb7ahLzwEw5R4D70J6RCYnhwIoldvAUb5cgU 18 | qlo0QAI6znlQYvV8g4A7uNFNiMTyFFY0fCNwX3IPiJQ/uQlSlrMydoPnXChdSvpZ 19 | yc4DScnuKLPOpHjDrf3mk5e3oILhDbwrqP7gmje9ySqh034VmtoPYYYkN5cxfPnH 20 | ZgxH7QaTpxI2KqH750NKKdWHJlcrwI09V0kWeJv9SsDx7ASlRoABhTUHhDXM1q3D 21 | NJwbYNNCqtlvtsUWRYptwZoOBjoOSLLHaHH2H+oT8JFszyTENVZ4JyoSEkcpd2lM 22 | uQGNBGCQlVIBDADRjXqoG6D16zegPZOk7KR1Zqdulyr1PaeAuvX6+8S+3wN1EWFx 23 | 4X6RoHOs0+ctL4CThL/6g/q7KHERvO6FJuWf7tmqp28zZcycToHnKstuRAnAmKBt 24 | 4Z4fMAnyz0FT60x51+M0O65tHWkpO/+VXY1WdjHHv99V5WT4b60VfB3UANYnjZAb 25 | oJahmMp6xv/PulJ7/2AvC+hGIe+zUR9FtGmjfVd+mARfWi0OE/z9kCjPyzL4lyjI 26 | gvvJiMG9dxkoVjGXfUKi9kXOXmazi/wBzZoHjpx8BXnR0p/LUXrE/sNv8hGnnnJI 27 | CtivwjhOeF2oiCFHSvFMNBCacVNoLdKpj5xEzs+xYNuRhDB4JJg81xmCKxXOHmBf 28 | x8NE5b4WTvIGhXrcS+sWK3/DCPOUtzb/Yi8tAlmljsUAWdJoJyHosTzGywSzryOw 29 | l2OliM+fwAC20TlcJBy9GlnOEl8Gllrz+3aYnVtzEJ+dHUt6amMZ5w7G91lU+KZD 30 | Ff5RDot2kJr7VScAEQEAAYkBtgQYAQoAIBYhBEZzm3S1atiPFLCILsfvcJAHJgEZ 31 | BQJgkJVSAhsMAAoJEMfvcJAHJgEZb/4L/35KljNNS1rP6m9D64uzh9zjmMAtiYhr 32 | cl8+GabNk/EwB37DZdG+YveULRXCEX8btdVQf9kZMStZ5dXx2i3hDMzpKOkKkacO 33 | qcWvfpmOvz9HDH4zijo0cGwmwt3eoWFadWgetOOSebTo/80+eeBLunPmL+clsX8i 34 | IgPMszWV6o/h6Gk37pGVT63TW24fhbeo+0M0ZiZQ24xMCyZcCyNX4+nCcii2aKuI 35 | r8YD6FOpokv/fsqpMt2dmRCsPNm34eB4ELSvpN/f+l1EqkW3VEouIapeVPWtC9Hb 36 | +nGv6LfYR8fBQ/Qzu+SvZUxv8FkudzP6PQFkbLL7DUwlmHleSsqGtFhp6dCyYdwB 37 | BbSa8Hz0cLd7GYo3Y8jNL/0BJAWdX971BI+7z30niZDYponV5tSz58XQc8umHM93 38 | Nh8iaYVptNSwDRTt//FBA+DwABYVNL4t4q2IHpsIKmH3cQuitSPH4oG0LzWUSutk 39 | lmOueMWQgP/hSqowukz7nv+wzY0RSweGQg== 40 | =y6CL 41 | -----END PGP PUBLIC KEY BLOCK----- 42 | -------------------------------------------------------------------------------- /src/main/resources/native/linux/aarch64/libsecp256k1.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparrowwallet/drongo/13e1fafbe8892d7005a043ae561e09ed66f7cea6/src/main/resources/native/linux/aarch64/libsecp256k1.so -------------------------------------------------------------------------------- /src/main/resources/native/linux/x64/libsecp256k1.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparrowwallet/drongo/13e1fafbe8892d7005a043ae561e09ed66f7cea6/src/main/resources/native/linux/x64/libsecp256k1.so -------------------------------------------------------------------------------- /src/main/resources/native/osx/aarch64/libsecp256k1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparrowwallet/drongo/13e1fafbe8892d7005a043ae561e09ed66f7cea6/src/main/resources/native/osx/aarch64/libsecp256k1.dylib -------------------------------------------------------------------------------- /src/main/resources/native/osx/x64/libsecp256k1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparrowwallet/drongo/13e1fafbe8892d7005a043ae561e09ed66f7cea6/src/main/resources/native/osx/x64/libsecp256k1.dylib -------------------------------------------------------------------------------- /src/main/resources/native/windows/x64/libsecp256k1-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparrowwallet/drongo/13e1fafbe8892d7005a043ae561e09ed66f7cea6/src/main/resources/native/windows/x64/libsecp256k1-0.dll -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/crypto/Argon2KeyDeriverTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import com.sparrowwallet.drongo.Utils; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.nio.charset.StandardCharsets; 8 | import java.security.SecureRandom; 9 | 10 | public class Argon2KeyDeriverTest { 11 | @Test 12 | public void noPasswordTest() { 13 | String password = ""; 14 | 15 | Argon2KeyDeriver.Argon2Parameters testParams = Argon2KeyDeriver.TEST_PARAMETERS; 16 | byte[] salt = new byte[testParams.saltLength]; 17 | Argon2KeyDeriver keyDeriver = new Argon2KeyDeriver(salt); 18 | Key key = keyDeriver.deriveKey(password); 19 | 20 | String hex = Utils.bytesToHex(key.getKeyBytes()); 21 | Assertions.assertEquals("6f6600a054c0271b96788906f62dfb1323c37b761715a0ae95ac524e4e1f2811", hex); 22 | } 23 | 24 | @Test 25 | public void testArgon2() { 26 | String password = "thisisapassword"; 27 | 28 | Argon2KeyDeriver keyDeriver = new Argon2KeyDeriver(Argon2KeyDeriver.TEST_PARAMETERS); 29 | Key key = keyDeriver.deriveKey(password); 30 | 31 | KeyCrypter keyCrypter = new AESKeyCrypter(); 32 | 33 | String message = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; 34 | byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); 35 | 36 | byte[] iv = new byte[16]; 37 | SecureRandom secureRandom = new SecureRandom(); 38 | secureRandom.nextBytes(iv); 39 | 40 | EncryptedData encrypted = keyCrypter.encrypt(messageBytes, iv, key); 41 | 42 | //Decrypt 43 | 44 | Argon2KeyDeriver keyDeriver2 = new Argon2KeyDeriver(Argon2KeyDeriver.TEST_PARAMETERS, encrypted.getKeySalt()); 45 | Key key2 = keyDeriver2.deriveKey(password); 46 | 47 | byte[] decrypted = keyCrypter.decrypt(encrypted, key2); 48 | String decryptedMessage = new String(decrypted, StandardCharsets.UTF_8); 49 | 50 | Assertions.assertEquals(message, decryptedMessage); 51 | } 52 | 53 | // @Test 54 | // public void findIterations() { 55 | // Argon2 argon2 = Argon2Factory.create(); 56 | // // 1000 = The hash call must take at most 1000 ms 57 | // // 65536 = Memory cost 58 | // // 1 = parallelism 59 | // int iterations = Argon2Helper.findIterations(argon2, 500, 256*1024, 4); 60 | // 61 | // System.out.println("Optimal number of iterations: " + iterations); 62 | // } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/crypto/BIP38Test.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.UnsupportedEncodingException; 7 | import java.security.GeneralSecurityException; 8 | 9 | public class BIP38Test { 10 | @Test 11 | public void testNoCompressionNoEC() throws GeneralSecurityException, UnsupportedEncodingException { 12 | Assertions.assertEquals("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR", BIP38.decrypt("TestingOneTwoThree", "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg").toString()); ; 13 | Assertions.assertEquals("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5", BIP38.decrypt("Satoshi", "6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq").toString()); ; 14 | } 15 | 16 | @Test 17 | public void testCompressionNoEC() throws GeneralSecurityException, UnsupportedEncodingException { 18 | Assertions.assertEquals("L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP", BIP38.decrypt("TestingOneTwoThree", "6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo").toString()); ; 19 | Assertions.assertEquals("KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7", BIP38.decrypt("Satoshi", "6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7").toString()); ; 20 | } 21 | 22 | @Test 23 | public void testCompressionEC() throws GeneralSecurityException, UnsupportedEncodingException { 24 | Assertions.assertEquals("5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", BIP38.decrypt("TestingOneTwoThree", "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX").toString()); ; 25 | Assertions.assertEquals("5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", BIP38.decrypt("Satoshi", "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd").toString()); ; 26 | } 27 | 28 | @Test 29 | public void testCompressionECLot() throws GeneralSecurityException, UnsupportedEncodingException { 30 | Assertions.assertEquals("5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", BIP38.decrypt("MOLON LABE", "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j").toString()); ; 31 | Assertions.assertEquals("5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D", BIP38.decrypt("ΜΟΛΩΝ ΛΑΒΕ", "6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH").toString()); ; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypterTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | 8 | public class ECIESKeyCrypterTest { 9 | @Test 10 | public void encryptDecrypt() { 11 | String testMessage = "thisisatestmessage"; 12 | byte[] testMessageBytes = testMessage.getBytes(StandardCharsets.UTF_8); 13 | byte[] initializationVector = "BIE1".getBytes(StandardCharsets.UTF_8); 14 | 15 | AsymmetricKeyDeriver keyDeriver = new Pbkdf2KeyDeriver(); 16 | ECKey key = keyDeriver.deriveECKey("iampassword"); 17 | 18 | AsymmetricKeyCrypter keyCrypter = new ECIESKeyCrypter(); 19 | EncryptedData encryptedData = keyCrypter.encrypt(testMessageBytes, initializationVector, key); 20 | byte[] crypterDecrypted = keyCrypter.decrypt(encryptedData, key); 21 | 22 | String cryDecStr = new String(crypterDecrypted, StandardCharsets.UTF_8); 23 | Assertions.assertEquals(testMessage, cryDecStr); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/crypto/ECKeyTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import com.sparrowwallet.drongo.KeyPurpose; 4 | import com.sparrowwallet.drongo.address.Address; 5 | import com.sparrowwallet.drongo.policy.Policy; 6 | import com.sparrowwallet.drongo.policy.PolicyType; 7 | import com.sparrowwallet.drongo.protocol.ScriptType; 8 | import com.sparrowwallet.drongo.wallet.*; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class ECKeyTest { 13 | @Test 14 | public void testGrindLowR() throws MnemonicException { 15 | String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; 16 | DeterministicSeed seed = new DeterministicSeed(words, "", 0, DeterministicSeed.Type.BIP39); 17 | Wallet wallet = new Wallet(); 18 | wallet.setPolicyType(PolicyType.SINGLE); 19 | wallet.setScriptType(ScriptType.P2PKH); 20 | Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation()); 21 | wallet.getKeystores().add(keystore); 22 | wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1)); 23 | 24 | WalletNode firstReceive = wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next(); 25 | Address address = firstReceive.getAddress(); 26 | Assertions.assertEquals("14JmU9a7SzieZNEtBnsZo688rt3mGrw6hr", address.toString()); 27 | ECKey privKey = keystore.getKey(firstReceive); 28 | 29 | //1 attempt required for low R 30 | String signature1 = privKey.signMessage("Test2", ScriptType.P2PKH); 31 | Assertions.assertEquals("IHra0jSywF1TjIJ5uf7IDECae438cr4o3VmG6Ri7hYlDL+pUEXyUfwLwpiAfUQVqQFLgs6OaX0KsoydpuwRI71o=", signature1); 32 | 33 | //2 attempts required for low R 34 | String signature2 = privKey.signMessage("Test", ScriptType.P2PKH); 35 | Assertions.assertEquals("IDgMx1ljPhLHlKUOwnO/jBIgK+K8n8mvDUDROzTgU8gOaPDMs+eYXJpNXXINUx5WpeV605p5uO6B3TzBVcvs478=", signature2); 36 | 37 | //3 attempts required for low R 38 | String signature3 = privKey.signMessage("Test1", ScriptType.P2PKH); 39 | Assertions.assertEquals("IEt/v9K95YVFuRtRtWaabPVwWOFv1FSA/e874I8ABgYMbRyVvHhSwLFz0RZuO87ukxDd4TOsRdofQwMEA90LCgI=", signature3); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/crypto/ScryptKeyDeriverTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.crypto; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.security.SecureRandom; 8 | 9 | public class ScryptKeyDeriverTest { 10 | @Test 11 | public void testScrypt() { 12 | ScryptKeyDeriver scryptKeyDeriver = new ScryptKeyDeriver(); 13 | Key key = scryptKeyDeriver.deriveKey("pass"); 14 | 15 | KeyCrypter keyCrypter = new AESKeyCrypter(); 16 | 17 | String message = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; 18 | byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); 19 | 20 | byte[] iv = new byte[16]; 21 | SecureRandom secureRandom = new SecureRandom(); 22 | secureRandom.nextBytes(iv); 23 | 24 | EncryptedData scrypted = keyCrypter.encrypt(messageBytes, iv, key); 25 | 26 | //Decrypt 27 | 28 | ScryptKeyDeriver scryptKeyDeriver2 = new ScryptKeyDeriver(scrypted.getKeySalt()); 29 | Key key2 = scryptKeyDeriver2.deriveKey("pass"); 30 | 31 | byte[] sdecrypted = keyCrypter.decrypt(scrypted, key2); 32 | String decryptedMessage = new String(sdecrypted, StandardCharsets.UTF_8); 33 | 34 | Assertions.assertEquals(message, decryptedMessage); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/uri/BitcoinUriTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.uri; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Locale; 7 | 8 | public class BitcoinUriTest { 9 | @Test 10 | public void testSamourai() throws BitcoinURIParseException { 11 | String uri = "bitcoin:BC1QT4NRM47695YWDG9N30N68JARMXRJNKFMR36994?amount=0,001"; 12 | BitcoinURI bitcoinURI = new BitcoinURI(uri); 13 | 14 | Assertions.assertEquals("BC1QT4NRM47695YWDG9N30N68JARMXRJNKFMR36994".toLowerCase(Locale.ROOT), bitcoinURI.getAddress().toString()); 15 | Assertions.assertEquals(Long.valueOf(100000), bitcoinURI.getAmount()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/wallet/DeterministicSeedTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.KeyDerivation; 4 | import com.sparrowwallet.drongo.crypto.KeyDeriver; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class DeterministicSeedTest { 9 | @Test 10 | public void testEncryption() { 11 | String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; 12 | 13 | DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39); 14 | KeyDeriver keyDeriver = seed.getEncryptionType().getDeriver().getKeyDeriver(); 15 | DeterministicSeed encryptedSeed = seed.encrypt(keyDeriver.deriveKey("pass")); 16 | 17 | DeterministicSeed decryptedSeed = encryptedSeed.decrypt("pass"); 18 | Assertions.assertEquals(words, decryptedSeed.getMnemonicString().asString()); 19 | } 20 | 21 | @Test 22 | public void testBip39Vector1() throws MnemonicException { 23 | String words = "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always"; 24 | 25 | DeterministicSeed seed = new DeterministicSeed(words, "TREZOR", 0, DeterministicSeed.Type.BIP39); 26 | Keystore keystore = Keystore.fromSeed(seed, KeyDerivation.parsePath("m/0'")); 27 | Assertions.assertEquals("xprv9s21ZrQH143K3VPCbxbUtpkh9pRG371UCLDz3BjceqP1jz7XZsQ5EnNkYAEkfeZp62cDNj13ZTEVG1TEro9sZ9grfRmcYWLBhCocViKEJae", keystore.getExtendedMasterPrivateKey().toString()); 28 | } 29 | 30 | @Test 31 | public void testBip39Vector2() throws MnemonicException { 32 | String words = "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside"; 33 | 34 | DeterministicSeed seed = new DeterministicSeed(words, "TREZOR", 0, DeterministicSeed.Type.BIP39); 35 | Keystore keystore = Keystore.fromSeed(seed, KeyDerivation.parsePath("m/0'")); 36 | Assertions.assertEquals("xprv9s21ZrQH143K2WNnKmssvZYM96VAr47iHUQUTUyUXH3sAGNjhJANddnhw3i3y3pBbRAVk5M5qUGFr4rHbEWwXgX4qrvrceifCYQJbbFDems", keystore.getExtendedMasterPrivateKey().toString()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.protocol.ScriptType; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class KeystoreTest { 8 | @Test 9 | public void testExtendedPrivateKey() throws MnemonicException { 10 | Keystore keystore = new Keystore(); 11 | DeterministicSeed seed = new DeterministicSeed("absent essay fox snake vast pumpkin height crouch silent bulb excuse razor", "", 0, DeterministicSeed.Type.BIP39); 12 | keystore.setSeed(seed); 13 | 14 | Assertions.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedMasterPrivateKey().toString()); 15 | } 16 | 17 | @Test 18 | public void testFromSeed() throws MnemonicException { 19 | ScriptType p2pkh = ScriptType.P2PKH; 20 | DeterministicSeed seed = new DeterministicSeed("absent essay fox snake vast pumpkin height crouch silent bulb excuse razor", "", 0, DeterministicSeed.Type.BIP39); 21 | Keystore keystore = Keystore.fromSeed(seed, p2pkh.getDefaultDerivation()); 22 | 23 | Assertions.assertEquals("xpub6D9jqMkBdgTqrzTxXVo2w8yZCa7HvzJTybFevJ2StHSxBRhs8dzsVEke9TQ9QjZCKbWZvzbc8iSScBbsCiA11wT28hZmCv3YmjSFEqCLmMn", keystore.getExtendedPublicKey().toString()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/sparrowwallet/drongo/wallet/PolicyTest.java: -------------------------------------------------------------------------------- 1 | package com.sparrowwallet.drongo.wallet; 2 | 3 | import com.sparrowwallet.drongo.policy.Policy; 4 | import com.sparrowwallet.drongo.policy.PolicyType; 5 | import com.sparrowwallet.drongo.protocol.ScriptType; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.List; 10 | import java.util.Locale; 11 | 12 | public class PolicyTest { 13 | @Test 14 | public void testMiniscriptParsing() { 15 | Keystore keystore1 = new Keystore("Keystore 1"); 16 | Keystore keystore2 = new Keystore("Keystore 2"); 17 | Keystore keystore3 = new Keystore("Keystore 3"); 18 | 19 | Policy policy = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, List.of(keystore1), 1); 20 | Assertions.assertEquals("pkh(keystore1)", policy.getMiniscript().toString().toLowerCase(Locale.ROOT)); 21 | Assertions.assertEquals(1, policy.getNumSignaturesRequired()); 22 | 23 | Policy policy2 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, List.of(keystore1), 1); 24 | Assertions.assertEquals("sh(wpkh(keystore1))", policy2.getMiniscript().toString().toLowerCase(Locale.ROOT)); 25 | Assertions.assertEquals(1, policy2.getNumSignaturesRequired()); 26 | 27 | Policy policy3 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, List.of(keystore1), 1); 28 | Assertions.assertEquals("wpkh(keystore1)", policy3.getMiniscript().toString().toLowerCase(Locale.ROOT)); 29 | Assertions.assertEquals(1, policy3.getNumSignaturesRequired()); 30 | 31 | Policy policy4 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, List.of(keystore1, keystore2, keystore3), 2); 32 | Assertions.assertEquals("sh(sortedmulti(2,keystore1,keystore2,keystore3))", policy4.getMiniscript().toString().toLowerCase(Locale.ROOT)); 33 | Assertions.assertEquals(2, policy4.getNumSignaturesRequired()); 34 | 35 | Policy policy5 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, List.of(keystore1, keystore2, keystore3), 2); 36 | Assertions.assertEquals("sh(wsh(sortedmulti(2,keystore1,keystore2,keystore3)))", policy5.getMiniscript().toString().toLowerCase(Locale.ROOT)); 37 | Assertions.assertEquals(2, policy5.getNumSignaturesRequired()); 38 | 39 | Policy policy6 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, List.of(keystore1, keystore2, keystore3), 2); 40 | Assertions.assertEquals("wsh(sortedmulti(2,keystore1,keystore2,keystore3))", policy6.getMiniscript().toString().toLowerCase(Locale.ROOT)); 41 | Assertions.assertEquals(2, policy6.getNumSignaturesRequired()); 42 | } 43 | } 44 | --------------------------------------------------------------------------------