Flutter2022. 8. 2. 16:27

Dart 언어 둘러보기

 

자세히 알아볼 시간이 없다.. 

다른 언어 공부할때 처럼 휘리릭 둘러보자. 자세한건 코딩하면서 …

 

c++ 비슷하면서 kotlin같기도 하고 머 그렇다.

 

void main() {
  print(‘Hello, World!’);

}

타입추론이 적용되고 타입체크가 강력하게 이루어진다.

 

var name = ‘Voyager I’; // 문자열

var year = 1977; // 숫자

var antennaDiameter = 3.7; // 부동소수

var flybyObjects = [‘Jupiter’, ‘Saturn’, ‘Uranus’, ‘Neptune’]; // 배열

var image = { // 딕셔너리

  ‘tags’: [‘stern’],

  ‘url’: ‘//path/to/saturn.jpg’

};

흐름제어

 

 if (year >= 2001) {
   print(‘21st century’);
 } else if (year >= 1901) {
   print(‘20st century’);
 }

for (final object in flybyObjects) {
  print(object)
}

for  (int month=1; month<=12; month++) {
  print(month);
}

While (year < 2016) {
  year += 1;
}

 

함수

 

int fibonacci(int n) {
  if (n=0 || n==1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

var result = fibonacci(20);

스트림함수호출을 아래와 같이 할 수 있으며 함수를 인자로 사용할수도 있다. 아래 코드에서 forEach의 print가 함수 인자로 사용된것을 볼수 있다.

 

flybyObjects.where((name) => name.contains(‘turn’)).forEach(print);

 

import

 

// Dart core lib 포함
import ‘daar:math’;

// 외부 패키지 라이브러리 포함
Import ‘package:test/test.dart’;

// 다른 파일 포함
Import ‘path/to/my_other_file.dart’;

클래스

 

class Spacecraft {
  String name;
  DateTime? launchDate;  // nullable
  int? get launchYear => launchDate?.year;   // getter
  Spacecraft(this.name, this.launchDate) {. // 생성자.
  }
  Spacecraft.unlaunched(String name) : this(name, null); // 이름을 갖는 생성자.

  // 메서드
  void describe() {
    print(‘Spacecraft: $name’);
    var launchDate = this.launchDate;
    if (launchDate != null) {
      int years = DateTime.now().difference(launchDate).inDays ~/ 365;
      print(‘Launched: $launchYear ($years years ago)’);
    } else {
      print(‘Unlaunched’);
    }
  }
}

var voyager = Spacecraft(‘Voyager I’, DateTime(1977,  9, 5));
voyager.describe();

var voyager3 = Spacecrafe.unlaunched(‘Voyager III’);
voyager3.describe();

 

열거자(Enums)

 

// 일반 열거자
enum PlanetType { terrestrial, gas, ice }

// 고급열거자
enum Planet {
  mercury(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
  venus(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
  // … 
  uranus(planetType: PlanetType.ice, moons, 27, hasRings: true),
  neptune(planetType: PlanetType.ice, moons: 14, hasRings: true;
  //  constant 생성자
  const Planet ( { required this.planetType, required this.moons, required this.hasRings } );

  final PlanetType planetType;
  final int moons;
  final bool hasRings;
}

usage

final yourPlanet = Planet.earth;
if (!yourPlanet.hasRings) {
  print(‘Your planet doesn’t has Rings’);
}

 

클래스 상속

 

class Orbiter extends Spacecraft {
  double altitude;
  Orbiter(super.name, DateTime super.launchDate, this.altitude);
}

Mixins

 

다중클래스 구조에서 코드를 재사용하는 방법을 제공한다. C++의 다중상속이 복잡함때문에 다른 언어는 다 삭제 했는데 얘는 왜 있는건지 모르겠다.

 

mixin Piloted {
  int astronauts = 1;
  void describeCrew() {
    print(‘Number of astronauts: $astronauts’);
  }
}
class PilotedCraft extends Spacecraft with Piloted {
  …
}

PilotedCraft가 astronauts필드를 갖게 되었고 describeCrew()호출이 가능해졌다.

 

interface

 

dart는 interface 키워드가 없으며 모든 클래스는 기본적으로 인터페이스로 간주해도 된다.

 

class MockSpaceCraft implements Spacecraft {
  // …
}

mixin과 implements와 머가 다를까?

일단 implements만으로도 클래스의 다중상속이 가능해진다.

extends로는 다중상속이 안된다. implements와 mixin을 혼용하니 컴파일오류가 난다.

흠 다중상속을 오랫동안 잊고 살았는데 다트에서 부활하니 참 당황스럽네..

왜 이런걸 이런식으로 지원하는지 언어 설계자의 머리속에 들어가보고 싶다.

mixin설계자와 implements설계자가 다른걸까?

 

abstract 클래스는 그리 헤깔리지 않게 지원한다.

 

abstract class Describable {
  void describe();
  void describeWithEmphasis() {
    print(‘===========‘);
    describe();
    print(‘===========‘);
  }
}

비동기호출

 

Future를 제공하여 비동기 호출을 지원한다.

 

const oneSecond = Duration(seconds: 1);

Future<void> printWithDelay(String message) async {
  await Future.delayed(oneSecond);
  print(message);
}

위 코드는 다음코드와 동일하다.

 

Future<void> printWithDelay(String message) {
  return Future.delayed(oneSecond).then((_) {
    print(message);
  });
}

아래 코드를 보면 async, await의 사용이 코드를 얼마나 읽기 쉽게 해주는지 확인할 수 있다.

 

Future<void> createDescriptions(Iterable<String> objects) async {
  for (final object in objects) {
    try {
      var file = File(‘$object.txt’);
      if (await file.exists()) {
        var modified = await file.lastModified();
        print(‘FIle for $object aleady exists. It was modified on $modified.’);
        continue;
      } 
      await file.create();
      await file.writeAsString(‘Start describing $object in this file.’);
    } on IOException catch (e) {
      print(‘Cannot create description for $object : %e’);
    }
  }
}

async*를 사용하여 더 나이스한 코딩이 가능하다.

 

Stream<String> report(Spacecraft craft, Iterable<String> objects) async* {
  for (final object in objects) {
    await Future.delayed(oneSecond);
    yield ‘${craft.name} flies by $object’);
  }
}

예외

 

예외가 필요하면 예외를 아래와 같이 발생시킨다.

 

if (astronauts == 0) {
  throw StateError(‘No astronauts.’);
}

try {
  for (final object in flybyObjects) {
    var description = await File($object.txt).readAsString();
    print(description);
  }
} on IOException catch (e) {
  print(‘Could not describe object: $e’);
} finally {
  flybyObjects.clear();
}

 

함수 파라메터 

 

함수를 인자로 넘길 수 있으며 이를 통해 콜백함수를 사용할 수 있다.

 

void getResultWithCallback(void onResult(String msg)) async {
  await Duration(seconds: 5);
  onResult('on result!!!');
}

getResultWithCallback((ret)=> {
  print(ret)
});

 

 

아주 아주 퀵하게 훓어 보았다.

 

Posted by 삼스
Android2022. 8. 2. 11:49

최근 구글 등록중에 아래와 같은 오류 메시지를 받았다.

 

잠시 당황??? 하고 .. 알아보았는데..

 

AES암호화 시 키유출에 대한 안드로이드의 잠재적 위험성이 이미 유명해져서 더이상 방관하지 않겠다는 구글의 의지를 확인하게 되었다.

 

해킹기술도 많이 알려져서 쉽게 가능한것도 문제의 원인인데 

  1. 안드로이드 디컴파일을 통한 코드 확인 가능 : 해커들은 난독화를 거쳤더라도 어떻게는 패턴을 찾아 키를 알아내려고하고 생각보다 쉽다.
  2. jni로 작성하더라도 objdump를 통해 .data, .text섹션을 뒤지다 보면 발견도 가능
  3. SharedPreference 로 작성된 xml파일을 내부저장소에 저장 하더라도 추출 가능

 

위 방법으로 또 알아낼수도 있다. ECB가 아니라 CBC알고리즘을 사용하여 IV가 추가로 필요하더라도 IV도 동일한 방법으로 유추가 가능.

 

이러저러한 이유로 아뭏든 못 믿겠다고 구글은 판단하고 KeyStore를 사용하란다.

 

이제 KeyStore에 대해 알아보겠다.

 

문서(https://developer.android.com/training/articles/keystore)에 따르면 “Android KeyStore”시스템을 사용하면 암호화키를 컨테이너안에 저장하여 기기에서 키를 추출하기 어렵게 할수 있고, 키저장소에 키가 저장되면 키자료(key material)은 유출이 안되면서 암호화작업에 사용이 가능하다. 시스템에서 키사용시가나 방법을 제한할수도 있다. 즉 키사용을 위해 사용자 인증을 요구하거나 특정암호화모드에서만 해당 키를 사용하도록 제한할수도 있다”

 

키저장소라는 이름에서 알수 있듯이 내가 사용하고자 하는 키를 안전하게 저장해주고 내가 사용하고 싶은때 꺼내서 사용할수 있다는것이다. 이제 키의 안정성에 대해 나는 완전히 안전하게 사용하고 있다고 당당하게 말할 수 있게 해줬다는 얘기이다.

 

아래 링크에 완전히 대체 가능한 코드샘플이 있다.

키를 RSA로 키쌍으로 생성하고 개인키로 암호화하고 공개키로 복호화하는 방식을 사용하였다.

 

https://gist.github.com/FrancescoJo/b8280cff14f1254f2185a9c2e927565e

 

import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import timber.log.Timber
import java.math.BigInteger
import java.security.GeneralSecurityException
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.Security
import java.security.spec.RSAKeyGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec.F4
import java.util.*
import javax.crypto.Cipher
import javax.security.auth.x500.X500Principal

/**
 * String encryption/decryption helper with system generated key.
 *
 * Results generated by this class are very difficult but not impossible to break. Since Android is
 * easy to decompile and attacker knows how the key generation and usage is implemented. It means
 * replay attack is still possible so attackers have a reliable chance better than brute force.
 *
 * Therefore, do not plant any values on this class which maybe used as attack vectors - such as
 * unique device identifiers(MAC address, OS version, etc.), nano timestamp, constants not related
 * to cipher specifications, etc.
 *
 * @author Francesco Jo(nimbusob@gmail.com)
 * @since 25 - May - 2018
 */
object AndroidRsaCipherHelper {
    /** All inputs are must be shorter than 2048 bits(256 bytes) */
    private const val KEY_LENGTH_BIT = 2048

    // Let's think about this problem in 2043
    private const val VALIDITY_YEARS = 25

    private const val KEY_PROVIDER_NAME = "AndroidKeyStore"
    private const val CIPHER_ALGORITHM =
            "${KeyProperties.KEY_ALGORITHM_RSA}/" +
            "${KeyProperties.BLOCK_MODE_ECB}/" +
            KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1

    private lateinit var keyEntry: KeyStore.Entry

    // Private only backing field
    @Suppress("ObjectPropertyName")
    private var _isSupported = false

    val isSupported: Boolean
        get() = _isSupported
    
    private lateinit var appContext: Context

    internal fun init(applicationContext: Context) {
        if (isSupported) {
            Timber.w("Already initialised - Do not attempt to initialise this twice")
            return
        }

        this.appContext = applicationContext
        val alias = "${appContext.packageName}.rsakeypairs"
        val keyStore = KeyStore.getInstance("AndroidKeyStore").apply({
            load(null)
        })

        val result: Boolean
        result = if (keyStore.containsAlias(alias)) {
            true
        } else {
            Timber.v("No keypair for %s, creating a new one", alias)

            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1 && initAndroidM(alias)) {
                true
            } else {
                initAndroidL(alias)
            }
        }

        this.keyEntry = keyStore.getEntry(alias, null)
        _isSupported = result
    }

    private fun initAndroidM(alias: String): Boolean {
        try {
            with(KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEY_PROVIDER_NAME), {
                val spec = KeyGenParameterSpec.Builder(alias,
                        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
                        .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(KEY_LENGTH_BIT, F4))
                        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
                        .setDigests(KeyProperties.DIGEST_SHA512,
                                KeyProperties.DIGEST_SHA384,
                                KeyProperties.DIGEST_SHA256)
                        /*
                         * Setting true only permit the private key to be used if the user authenticated
                         * within the last five minutes.
                         */
                        .setUserAuthenticationRequired(false)
                        .build()
                initialize(spec)
                generateKeyPair()
            })
            Timber.i("Random keypair with %s/%s/%s is created.", KeyProperties.KEY_ALGORITHM_RSA,
                    KeyProperties.BLOCK_MODE_CBC, KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)

            return true
        } catch (e: GeneralSecurityException) {
            /*
             * Nonsense, but some devices manufactured by developing countries have actual problem
             * Consider using JCE substitutes like Spongy castle(Bouncy castle for android)
             */
            Timber.w(e, "It seems that this device does not support RSA algorithm!!")

            return false
        }
    }

    /**
     * Tested and verified working on Nexus 5s API Level 21, it is not guaranteed that this logic is valid on
     * all Android L devices.
     */
    private fun initAndroidL(alias: String): Boolean {
        try {
            with(KeyPairGenerator.getInstance("RSA", KEY_PROVIDER_NAME), {
                val start = Calendar.getInstance(Locale.ENGLISH)
                val end = Calendar.getInstance(Locale.ENGLISH).apply { add(Calendar.YEAR, VALIDITY_YEARS) }
                val spec = KeyPairGeneratorSpec.Builder(appContext)
                        .setKeySize(KEY_LENGTH_BIT)
                        .setAlias(alias)
                        .setSubject(X500Principal("CN=francescojo.github.com, OU=Android dev, O=Francesco Jo, L=Chiyoda, ST=Tokyo, C=JP"))
                        .setSerialNumber(BigInteger.ONE)
                        .setStartDate(start.time)
                        .setEndDate(end.time)
                        .build()
                initialize(spec)
                generateKeyPair()
            })
            Timber.i("Random RSA algorithm keypair is created.")

            return true
        } catch (e: GeneralSecurityException) {
            Timber.w(e, "It seems that this device does not support encryption!!")

            return false
        }
    }

    /**
     * Beware that input must be shorter than 256 bytes. The length limit of plainText could be dramatically
     * shorter than 256 letters in certain character encoding, such as UTF-8.
     */
    fun encrypt(plainText: String): String {
        if (!_isSupported) {
            return plainText
        }

        val cipher = Cipher.getInstance(CIPHER_ALGORITHM).apply({
            init(Cipher.ENCRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).certificate.publicKey)
        })
        val bytes = plainText.toByteArray(Charsets.UTF_8)
        val encryptedBytes = cipher.doFinal(bytes)
        val base64EncryptedBytes = Base64.encode(encryptedBytes, Base64.DEFAULT)

        return String(base64EncryptedBytes)
    }

    fun decrypt(base64EncryptedCipherText: String): String {
        if (!_isSupported) {
            return base64EncryptedCipherText
        }

        val cipher = Cipher.getInstance(CIPHER_ALGORITHM).apply({
            init(Cipher.DECRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).privateKey)
        })
        val base64EncryptedBytes = base64EncryptedCipherText.toByteArray(Charsets.UTF_8)
        val encryptedBytes = Base64.decode(base64EncryptedBytes, Base64.DEFAULT)
        val decryptedBytes = cipher.doFinal(encryptedBytes)

        return String(decryptedBytes)
    }
}

 

앱은 재등록하였고 이번엔 통과되었다. ^^

 

 

Posted by 삼스