안드로이드 KeyStore로 KeyRing 생성 문의

안녕하세요.
caver-java를 활용하여 klaytn wallet 기능 구현을 하려고 합니다.
블록체인에 대한 지식이 얕아서 혼란스럽지만 클레이튼 문서에 설명이 잘 되어 있어서 도움을 받으며 진행하고 있습니다.
시작부터 문제에 봉착했는데 관련하여 몇가지 문의드립니다.

  1. v1.5.0(~1.5.7)-android 사용하여 안드로이드 버전 19 이상을 사용하려 하는데
    키 생성 후 키스토어를 Secure SharedPreference에 저장 → 사용 시 불러오는 부분을 구현 중 KeyringFactory.decrypt(String keyStore, String password) 메소드 사용 시 안드로이드 19버전에서 NoSuchMethodError 발생합니다.
    26버전에서 정상적으로 실행되는걸 봤을때 버전문제가 맞는 것 같습니다.
    19버전에서 구현할 수 있는 방법이 있을까요?

  2. 검색중에 메소드 지원버전 정보가 있는 페이지를 봤던 것 같은데 다시 찾아보니 보이지 않아서 확인하는 방법도 알려주시면 감사하겠습니다.

  3. KeyringFactory.generate()로 키 생성 후 wallet을 사용함에 있어서 keyring 외에 따로 관리해야 할 데이터가 있을까요?(초보적인 질문 죄송합니다.)

안녕하세요. 클레이튼 포럼에 질문을 남겨주셔서 감사드립니다.

  1. caver-java-android는 java8의 문법을 사용하고 있습니다. Android에서 java8 이상의 문법을 사용하기 위한 문서를 참고 부탁드립니다.
  • 참고로 min sdk를 19로 설정해서 정상적으로 decrypt가 되는 것을 확인했습니다.
  1. 메서드 지원정보가 있는 페이지가 어떤걸 이야기하는 것인지 좀 더 자세히 설명 부탁드려도 될까요?

  2. caver-java에서 Wallet은 기본적으로 KeyringContainer 클래스의 instance로 구현되어있습니다. Keyring이외에 관리할 데이터는 따로 없습니다.

답변 감사드립니다.

해당 오류는 java8 설정이 되어 있는데 발생하고 있습니다.
아래는 제가 사용한 코드입니다.

<build.gradle(project)>

buildscript {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

<build.gradle(app)>

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.test.kleytnwallet"
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation 'com.klaytn.caver:core:1.5.7-android'
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation ("com.squareup.okhttp3:okhttp:3.12.12"){
        force = true
    }
    implementation 'com.squareup.okhttp3:logging-interceptor:3.12.12'

}

<사용 코드>

Caver caver = new Caver("https://api.baobab.klaytn.net:8651");
SingleKeyring keyring = KeyringFactory.generate();
KeyStore keyStore = null;
try {
     keyStore = keyring.encrypt("password");
     keyring = (SingleKeyring) KeyringFactory.decrypt(keyStore, "password");       // 오류 발생
} catch (CipherException e) {
     e.printStackTrace();
}

<오류내용>

E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.test.kleytnwallet, PID: 12199
java.lang.NoSuchMethodError: java.util.List.stream
    at com.klaytn.caver.wallet.keyring.KeyringFactory.decrypt(KeyringFactory.java:314)
    at com.klaytn.caver.wallet.keyring.KeyringFactory.decrypt(KeyringFactory.java:263)
    at com.test.kleytnwallet.MainActivity.createAccount(MainActivity.java:97)
    at com.test.kleytnwallet.MainActivity.onCreate(MainActivity.java:43)
    at android.app.Activity.performCreate(Activity.java:5231)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2159)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)
    at android.app.ActivityThread.access$800(ActivityThread.java:135)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1196)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:136)
    at android.app.ActivityThread.main(ActivityThread.java:5017)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
    at dalvik.system.NativeStart.main(Native Method)

2번 질문은 api 문서에서 봤던걸로 기억하는데 정확히 어디서 봤는지 기억이 안납니다.
예를 들면
public abstract AbstractKeyring copy() v23
public abstract AbstractKeyring sign() v23
public abstract AbstractKeyring encrypt() v19
이런식으로 정리가 되어있는걸 본 것 같습니다.

2번과 관련된 문서는 따로 없습니다.

그리고 전달해주신 코드를 참고삼아 해봤을때는 잘 동작하고있고, Iam님이 전달해주신 설정값과 비교해봤을때 다른 부분은 아래와 같습니다.

  • build.gradle(project)의 gradle version : classpath “com.android.tools.build:gradle:4.1.2”
  • build.gradle(:app)에서의 buildToolsVersion : buildToolsVersion “30.0.3”

위 설정값을 바꿔보시고 한번 실행 부탁드립니다.

저의 gradle 설정 파일과 Test 코드 공유드립니다.(JUNIT으로 Test를 진행했습니다.)

<build.gradle(project)>

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.2"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

<build.grade(:app)>

plugins {
    id 'com.android.application'
}

android {
    compileSdkVersion 29
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.example.myapplication"
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation 'com.klaytn.caver:core:1.5.7-android'
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation ("com.squareup.okhttp3:okhttp:3.12.12"){
        force = true
    }
    implementation 'com.squareup.okhttp3:logging-interceptor:3.12.12'

}

Test code

package com.example.myapplication;

import com.klaytn.caver.wallet.keyring.AbstractKeyring;
import com.klaytn.caver.wallet.keyring.KeyringFactory;
import com.klaytn.caver.wallet.keyring.MultipleKeyring;
import com.klaytn.caver.wallet.keyring.PrivateKey;
import com.klaytn.caver.wallet.keyring.RoleBasedKeyring;
import com.klaytn.caver.wallet.keyring.SingleKeyring;

import org.junit.Test;
import org.web3j.crypto.CipherException;

import java.io.IOException;
import java.util.Arrays;

import static org.junit.Assert.*;

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
public class ExampleUnitTest {

    String jsonV4 ="{\n" +
            "  \"version\":4,\n" +
            "  \"id\":\"55da3f9c-6444-4fc1-abfa-f2eabfc57501\",\n" +
            "  \"address\":\"0x86bce8c859f5f304aa30adb89f2f7b6ee5a0d6e2\",\n" +
            "  \"keyring\":[\n" +
            "    [\n" +
            "      {\n" +
            "        \"ciphertext\":\"93dd2c777abd9b80a0be8e1eb9739cbf27c127621a5d3f81e7779e47d3bb22f6\",\n" +
            "        \"cipherparams\":{\"iv\":\"84f90907f3f54f53d19cbd6ae1496b86\"},\n" +
            "        \"cipher\":\"aes-128-ctr\",\n" +
            "        \"kdf\":\"scrypt\",\n" +
            "        \"kdfparams\":{\n" +
            "          \"dklen\":32,\n" +
            "          \"salt\":\"69bf176a136c67a39d131912fb1e0ada4be0ed9f882448e1557b5c4233006e10\",\n" +
            "          \"n\":4096,\n" +
            "          \"r\":8,\n" +
            "          \"p\":1\n" +
            "        },\n" +
            "        \"mac\":\"8f6d1d234f4a87162cf3de0c7fb1d4a8421cd8f5a97b86b1a8e576ffc1eb52d2\"\n" +
            "      },\n" +
            "      {\n" +
            "        \"ciphertext\":\"53d50b4e86b550b26919d9b8cea762cd3c637dfe4f2a0f18995d3401ead839a6\",\n" +
            "        \"cipherparams\":{\"iv\":\"d7a6f63558996a9f99e7daabd289aa2c\"},\n" +
            "        \"cipher\":\"aes-128-ctr\",\n" +
            "        \"kdf\":\"scrypt\",\n" +
            "        \"kdfparams\":{\n" +
            "          \"dklen\":32,\n" +
            "          \"salt\":\"966116898d90c3e53ea09e4850a71e16df9533c1f9e1b2e1a9edec781e1ad44f\",\n" +
            "          \"n\":4096,\n" +
            "          \"r\":8,\n" +
            "          \"p\":1\n" +
            "        },\n" +
            "        \"mac\":\"bca7125e17565c672a110ace9a25755847d42b81aa7df4bb8f5ce01ef7213295\"\n" +
            "      }\n" +
            "    ],\n" +
            "    [\n" +
            "      {\n" +
            "        \"ciphertext\":\"f16def98a70bb2dae053f791882f3254c66d63416633b8d91c2848893e7876ce\",\n" +
            "        \"cipherparams\":{\"iv\":\"f5006128a4c53bc02cada64d095c15cf\"},\n" +
            "        \"cipher\":\"aes-128-ctr\",\n" +
            "        \"kdf\":\"scrypt\",\n" +
            "        \"kdfparams\":{\n" +
            "          \"dklen\":32,\n" +
            "          \"salt\":\"0d8a2f71f79c4880e43ff0795f6841a24cb18838b3ca8ecaeb0cda72da9a72ce\",\n" +
            "          \"n\":4096,\n" +
            "          \"r\":8,\n" +
            "          \"p\":1\n" +
            "        },\n" +
            "        \"mac\":\"38b79276c3805b9d2ff5fbabf1b9d4ead295151b95401c1e54aed782502fc90a\"\n" +
            "      }\n" +
            "    ],\n" +
            "    [\n" +
            "      {\n" +
            "        \"ciphertext\":\"544dbcc327942a6a52ad6a7d537e4459506afc700a6da4e8edebd62fb3dd55ee\",\n" +
            "        \"cipherparams\":{\"iv\":\"05dd5d25ad6426e026818b6fa9b25818\"},\n" +
            "        \"cipher\":\"aes-128-ctr\",\n" +
            "        \"kdf\":\"scrypt\",\n" +
            "        \"kdfparams\":{\n" +
            "          \"dklen\":32,\n" +
            "          \"salt\":\"3a9003c1527f65c772c54c6056a38b0048c2e2d58dc0e584a1d867f2039a25aa\",\n" +
            "          \"n\":4096,\n" +
            "          \"r\":8,\n" +
            "          \"p\":1\n" +
            "        },\n" +
            "        \"mac\":\"19a698b51409cc9ac22d63d329b1201af3c89a04a1faea3111eec4ca97f2e00f\"\n" +
            "      },\n" +
            "      {\n" +
            "        \"ciphertext\":\"dd6b920f02cbcf5998ed205f8867ddbd9b6b088add8dfe1774a9fda29ff3920b\",\n" +
            "        \"cipherparams\":{\"iv\":\"ac04c0f4559dad80dc86c975d1ef7067\"},\n" +
            "        \"cipher\":\"aes-128-ctr\",\n" +
            "        \"kdf\":\"scrypt\",\n" +
            "        \"kdfparams\":{\n" +
            "          \"dklen\":32,\n" +
            "          \"salt\":\"22279c6dbcc706d7daa120022a236cfe149496dca8232b0f8159d1df999569d6\",\n" +
            "          \"n\":4096,\n" +
            "          \"r\":8,\n" +
            "          \"p\":1\n" +
            "        },\n" +
            "        \"mac\":\"1c54f7378fa279a49a2f790a0adb683defad8535a21bdf2f3dadc48a7bddf517\"\n" +
            "      }\n" +
            "    ]\n" +
            "  ]\n" +
            "}";

    public static void checkValidKeyring(AbstractKeyring expect, AbstractKeyring actual) {
        assertEquals(expect.getAddress(), actual.getAddress());
        assertEquals(expect.getClass(), actual.getClass());

        if(expect instanceof SingleKeyring) {
            assertEquals(((SingleKeyring) expect).getKey().getPrivateKey(), ((SingleKeyring)actual).getKey().getPrivateKey());
        }

        if(expect instanceof MultipleKeyring) {
            MultipleKeyring expectKeyring = (MultipleKeyring)expect;
            MultipleKeyring actualKeyring = (MultipleKeyring)actual;

            for(int i=0; i<expectKeyring.getKeys().length; i++) {
                assertEquals(expectKeyring.getKeys().length, actualKeyring.getKeys().length);
            }
        }
        if(expect instanceof RoleBasedKeyring) {
            RoleBasedKeyring expectKeyring = (RoleBasedKeyring)expect;
            RoleBasedKeyring actualKeyring = (RoleBasedKeyring)actual;

            for(int i=0; i< expectKeyring.getKeys().size(); i++) {
                PrivateKey[] actualArr = actualKeyring.getKeys().get(i);
                PrivateKey[] expectedArr = expectKeyring.getKeys().get(i);

                assertEquals(expectedArr.length, actualArr.length);

                for(int j=0; j<actualArr.length; j++) {
                    assertEquals(expectedArr[j].getPrivateKey(), actualArr[j].getPrivateKey());
                }
            }
        }
    }


    @Test
    public void decrypt() throws CipherException, IOException {
        String password = "password";
        String expectedAddress = "0x86bce8c859f5f304aa30adb89f2f7b6ee5a0d6e2";
        String[][] expectedPrivateKeys = new String[][] {
                {
                        "0xd1e9f8f00ef9f93365f5eabccccb3f3c5783001b61a40f0f74270e50158c163d",
                        "0x4bd8d0b0c1575a7a35915f9af3ef8beb11ad571337ec9b6aca7c88ca7458ef5c",
                },
                {
                        "0xdc2690ac6017e32ef17ea219c2a2fd14a2bb73e7a0a253dfd69abba3eb8d7d91",
                },
                {
                        "0xf17bf8b7bee09ffc50a401b7ba8e633b9e55eedcf776782f2a55cf7cc5c40aa8",
                        "0x4f8f1e9e1466609b836dba611a0a24628aea8ee11265f757aa346bde3d88d548",
                }
        };

        AbstractKeyring expect = KeyringFactory.createWithRoleBasedKey(expectedAddress, Arrays.asList(expectedPrivateKeys));
        AbstractKeyring actual = KeyringFactory.decrypt(jsonV4, password);

        checkValidKeyring(expect, actual);
    }
}

해결되었습니다.
Desugaring 설정은 안해도 되는줄 알았는데 이것까지 해야 되더군요.
보여주신 코드에도 Desugaring은 사용하지 않으셨던데 무슨 차이로 그런건지 이해가 안되네요.ㅠㅠ
아무튼 잘 해결되었고, 문제해결에 도움주셔서 감사합니다!