기존에 SharedPreferences에서 저장시 plain text인 것을  EncryptedSharedPreferences를 이용하면 암호화되어 저장하는 기능을 제공합니다. 

 

자세한 내용은 

https://developer.android.com/topic/security/data?hl=ko

 

Android 개발자  |  Android Developers

더 안전하게 데이터 사용 Android Jetpack의 일부 보안 라이브러리는 저장 데이터 읽기 및 쓰기와 관련된 보안 권장사항의 구현과 키 생성 및 인증을 제공합니다. 라이브러리는 빌더 패턴을 사용하��

developer.android.com

Android 6.0(API 수준 23) 이상을 실행하는 기기에서 지원됩니다

 

 

App내에 ID, PASSWORD와 같은 중요정보를 AES 대칭키(암호화, 복호화 키 동일)를 이용하여  암호화를 했을때 문제점은 

키 관리였습니다.

프로젝트내에 하드코딩된 문자열은 보안상 취약하므로 노출의 위험이 있습니다.

 

EncryptedSharedPreferences를 이용하여 저장해 보도록 하겠습니다.

라이브러리 내부적으로 KeyStore를 이용하여 키 접근은 application레벨에서 접근이 안되어 안전하다고 볼 수 있습니다. 

https://developer.android.com/training/articles/keystore?hl=ko

 

Android Keystore 시스템  |  Android 개발자  |  Android Developers

Android Keystore 시스템을 사용하면 암호화 키를 컨테이너에 저장하여 기기에서 키를 추출하기 어렵게 할 수 있습니다. 키 저장소에 키가 저장되면, 키 자료는 내보낼 수 없는 상태로 유지하면서 키

developer.android.com

gradle

implementation "androidx.security:security-crypto:1.0.0-rc03"

 

테스트 코드

val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

val SharedPreferences = EncryptedSharedPreferences
        .create(
            "android",
            masterKeyAlias,
            requireContext(),
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )

val sharedPrefsEditor = SharedPreferences.edit()
sharedPrefsEditor?.apply {
    putString("id", "test")
    putString("password", "test")
}
sharedPrefsEditor.apply()

Log.d(TAG, SharedPreferences.getString("id", "not found").toString())

 

확인 XML 열어보기

/data/data/[packageName]/shared_prefs/android.xml 

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a90115c36d71e8bd3b39a8409886b09616e196b34e6df40b5a585f7223e34216ff542cb59dcd2759720826c95dad26a36aa7ce081d9e4eeddc9bc1aee8f1fb2cb473bf3d2acfb459e12cb760661372c8322e682cc7b5982ef9fccd6a9699643d75eca2b63adbaf166f5537cddded9c8b1c3daa0a8d7c68d88f292d9804b270878e281fb654ab77b40248725b91046cba9885af649384766684316b6be6813f351f7fb214391ba6c8d0a6fb1a44089cdaf29501123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b65791001189cdaf295012001</string>
    <string name="ARK8rRzpTKbecLHO3yCiMMBaFHa2xCLZAJhtLVE=">ASyO+WIJmBqi+YFJPHKfX8DDLv8bZfqjVrL59TgD3CVmAfLH9elUqE+8hmVA</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801c62e98509808338085528011581576775e3701e7483e4a9d27b89ba63e3406db1b8a6a71d9aee723173fe3feb24455eb6ddbc5ffcab51dc7b4e6b4dc831a4e3daf4f2e3612337734667cfd134949655e2d05623badf75f5cde42eb4148d6aed4edd5914779e0ee360c905c0ffa3192c11d8560bc78c5bc4d84ff651bfca54ba67c5786059ae4f1701a4408e2f2bbe402123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118e2f2bbe4022001</string>
    <string name="ARK8rRxdjEShL1rCHntSr1uL6GEkhB4=">ASyO+WKJ42K6D7TXNXO5Os3dxLGmJeGdlYVsIRYUVif85iONYyTlSkwWhzQl</string>
</map>

위와같이 암호화되어 저장되어 있는것을 확인하였습니다.

오토레이아웃을 코딩으로 구현시 코드를 간결하게 작성하기 위해 SnapKit을 사용해 보겠습니다.

스냅킷은 DSL로 오토레이아웃을 쉽게 사용할 수 있는 기능을 제공합니다.

관련 링크는 하기와 같습니다.

https://github.com/SnapKit/SnapKit

 

SnapKit/SnapKit

A Swift Autolayout DSL for iOS & OS X. Contribute to SnapKit/SnapKit development by creating an account on GitHub.

github.com

 

변경 작업은 이전에 포스팅했던

https://xmobile.tistory.com/entry/IOS-오토레이아웃-제약조건-코딩으로-사용하기

를 활용 하도록 하겠습니다.

 

변경 전

private func addconstraints() {
        var constraints = [NSLayoutConstraint]()
        
        constraints.append(box1.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box2.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box3.heightAnchor.constraint(equalToConstant: 100))
        //box2와 box3 너비 같도록 설정 
        constraints.append(box2.widthAnchor.constraint(equalTo: box3.widthAnchor))
        constraints.append(box3.widthAnchor.constraint(equalTo: box2.widthAnchor))
        
        //box1 left : 뷰콘트롤러뷰의 왼쪽에서 20만큼 떨어진 곳에 box1 left가 위치함
        constraints.append(box1.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box1 right : 뷰콘트롤러뷰의 오른쪽에서 20만큼 떨어진 곳에 box1 right가 위치함
        constraints.append(box1.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20))
        //box1 top :  뷰콘트롤러뷰의 위쪽에서 40만큼 떨어진 곳에 box1 top 위치함
        constraints.append(box1.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40))


        //box2 left : 뷰콘트롤러뷰의 왼쪽에서 20만큼 떨어진 곳에 box2 left가 위치함
        constraints.append(box2.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box2 top : box1의 아래쪽에서 40만큼 떨어진 곳에 box2 top 위치함
        constraints.append(box2.topAnchor.constraint(equalTo: box1.bottomAnchor, constant: 40))


        //box3 left : box2 오른쪽에서 20만큼 떨어진 곳에 box3 left가 위치함
        constraints.append(box3.leadingAnchor.constraint(equalTo: box2.trailingAnchor, constant: 20))
        //box3 right : 뷰콘트롤러뷰의 오른쪽에서 -20만큼 떨어진 곳에 box3 right가 위치함
        constraints.append(box3.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20))
        //box3 top :  box1의 아래쪽에서 40만큼 떨어진 곳에 box3 top 위치함
        constraints.append(box3.topAnchor.constraint(equalTo: box1.bottomAnchor, constant: 40))


        //Activate(Applying)
        NSLayoutConstraint.activate(constraints)
    }

 

변경 후

private func addconstraints() {
        box1.snp.makeConstraints { (make) -> Void in
            make.height.equalTo(100)
            make.leading.equalToSuperview().offset(20)
            make.trailing.equalToSuperview().offset(-20)
            make.top.equalToSuperview().offset(40)
        }
        
        box2.snp.makeConstraints { (make) -> Void in
            make.height.equalTo(100)
            make.width.equalTo(box3.snp.width)
            make.leading.equalToSuperview().offset(20)
            make.top.equalTo(box1.snp.bottom).offset(40)
        }
        
        box3.snp.makeConstraints { (make) -> Void in
            make.height.equalTo(100)
            make.width.equalTo(box2.snp.width)
            make.leading.equalTo(box2.snp.trailing).offset(20)
            make.trailing.equalToSuperview().offset(-20)
            make.top.equalTo(box1.snp.bottom).offset(40)
        }
    }

위와 같이 소스코드가 간결해 지는것을 확인하였습니다. 

이상입니다. 

 

Realm Swift를 이용하면 효율적으로 안전하고 빠르고 지속적인 방법으로 앱의 모델 레이어를 작성할 수 있습니다.

https://realm.io/kr/docs/swift/latest/

 

Realm: 리액티브 모바일 애플리케이션을 손쉽고 빠르게 만드세요

Realm Swift is the first database built for mobile. An alternative to SQLite and Core Data that's fast, easy to use, and open source.

realm.io

의존성 설정 

pod 'RealmSwift', '~> 3.20.0'

 

사용해 보기 

1. entity 클래스 

import Foundation
import RealmSwift

class LottoEntity: Object {
    @objc dynamic var lottoid: Int = 0
    @objc dynamic var desc = ""
    @objc dynamic var url = ""
    @objc dynamic var regDate: Double = 0.0
    @objc dynamic var modDate: Double = 0.0
    
    override static func primaryKey() -> String? {
        return "lottoid"
    }
}

2. crud 만들기

import Foundation
import RealmSwift

public class DatabaseManager {
    static let shared = DatabaseManager()
    private var realm: Realm
    
    private init() {
        realm = try! Realm()
    }
    
    //id자동증가를 위한 함수 
    private func newID() -> Int {
        return realm.objects(LottoEntity.self).count+1
    }
    
    //삽입
    func insert(lottoEntity: LottoEntity) {
        lottoEntity.lottoid = newID()
        lottoEntity.regDate = Date().currentTimeMillis()
        
        try! realm.write {
            realm.add(lottoEntity)
        }
    }
    
    //업데이트 
    func update(lottoId: Int, desc: String?, url: String?) {
        let lottoEntity = selectById(lottoId: lottoId)
        if let workout = lottoEntity {
            try! realm.write {
                if let desc = desc{
                    workout.desc = desc
                }
                if let url = url{
                    workout.url = url
                }
                workout.modDate = Date().currentTimeMillis()
            }
        }
    }
    
    //삭제 object로 
    func delete(lottoEntity: LottoEntity) {
        try! realm.write {
            realm.delete(lottoEntity)
        }
    }
    
    //삭제 id로 
    func deleteById(lottiId: Int) {
        try! realm.write {
            if let entity = selectById(lottoId: lottiId) {
                realm.delete(entity)
            }
        }
    }
    
    //삭제 all
    func deleteAll() {
        try! realm.write {
            realm.deleteAll()
        }
    }
    
    //가져오기 id로 
    func selectById(lottoId: Int) -> LottoEntity? {
        let predicate = NSPredicate(format: "lottoid == %i", lottoId)
        return realm.objects(LottoEntity.self).filter(predicate).first
    }
    
    //가져오기 all
    func selectAll() -> Results<LottoEntity> {
        return realm.objects(LottoEntity.self)
    }
}

extension Date {
    func currentTimeMillis() -> Double {
        return Double(self.timeIntervalSince1970 * 1000)
    }
}

3. UNIT 테스트로 검증해보기  

func testExample() throws {
        //all 삭제
        DatabaseManager.shared.deleteAll()
        
        //insert
        var entity = LottoEntity()
        entity.url = "url"
        entity.desc = "desc"
        DatabaseManager.shared.insert(lottoEntity: entity)
        XCTAssert(DatabaseManager.shared.selectAll().count == 1)
        
        //update
        //desc 값 변경
        DatabaseManager.shared.update(lottoId: 1, desc: "desc_change", url: nil)
        print(DatabaseManager.shared.selectAll())
        if let selEntity = DatabaseManager.shared.selectById(lottoId: 1) {
            XCTAssert(selEntity.desc == "desc_change")
        }
        
        //insert
        entity = LottoEntity()
        entity.url = "url1"
        entity.desc = "desc2"
        DatabaseManager.shared.insert(lottoEntity: entity)
        XCTAssert(DatabaseManager.shared.selectAll().count == 2)
        
        //delete
        if let entity = DatabaseManager.shared.selectAll().first {
            DatabaseManager.shared.delete(lottoEntity: entity)
        }
        print(DatabaseManager.shared.selectAll())
        XCTAssert(DatabaseManager.shared.selectAll().count == 1)
        
        //delete all
        DatabaseManager.shared.deleteAll()
        XCTAssert(DatabaseManager.shared.selectAll().count == 0)
        print(DatabaseManager.shared.selectAll())
    }

하기와 같이 정상적으로 동작하는것을 확인해 보았습니다. 

+ Recent posts