키링(Keyring)
키링은 메타마스크의 비밀 저장 및 계정 관리 시스템의 핵심 개념.
KeyringController = 키링의 구현
KeyringController README 일부
A module for managing groups of Ethereum accounts called "Keyrings", defined originally for MetaMask's multiple-account-type feature.
The KeyringController has three main responsibilities:
*Initializing & using (signing with) groups of Ethereum accounts ("keyrings").
*Keeping track of local nicknames for those individual accounts.
*Providing password-encryption persisting & restoring of secret information.
* 키링: seed phrase. 공개 키-개인 키 쌍을 생성
* 키: 시드 문구로부터 생성된 개인 키를 가지고 있는 각각의 지갑 계정
시드 문구(키링)와 모든 계정 데이터(키)는 사용자의 비밀번호로부터 생성된 비밀키로 암호화되어 함께 묶여 있음.
ObservableStore
- KeyringController는 ObservableStore(Vault) 클래스를 사용하여 데이터를 저장.
- ObservableStore는 단일 값에 대한 동기식 인메모리 저장소.
constructor (opts) {
super();
const initState = opts.initState || {};
this.keyringTypes = opts.keyringTypes ? keyringTypes.concat(opts.keyringTypes) : keyringTypes;
this.store = new ObservableStore(initState);
this.memStore = new ObservableStore({
isUnlocked: false,
keyringTypes: this.keyringTypes.map((krt: T) => krt.type),
keyrings: [],
});
this.encryptor = opts.encryptor || encryptor;
this.keyrings = [];
}
- KeyringController 생성자 내부에 this.store, this.memStore라는 이름의 ObservableStore 객체 생성
- this.store에 암호화된 지갑 secret 저장.
- this.store 데이터 크롬 확장 프로그램 로컬 저장소에 영구 저장
chrome.storage.local.get('data', (result) => {
var vault = result.data.KeyringController.vault;
console.log(vault);
})
- 크롬 확장 프로그램 개발자도구 콘솔에 위의 코드를 입력하여 데이터에 액세스 가능
- this.memStore에는 복호화된 지갑 secret을 저장
- this.memStore의 데이터는 메모리에 저장(영구 저장 X)
- MetaMask 확장 프로그램에 비밀번호 입력시 복호화된 계정 개인키가 this.memStore에 저장됨.
/**
* Submit Password
*
* Attempts to decrypt the current vault and load its keyrings
* into memory.
*
* @emits KeyringController#unlock
* @param {string} password - The keyring controller password.
* @returns {Promise<Object>} A Promise that resolves to the state.
*/
submitPassword (password) {
return this.unlockKeyrings(password)
.then((keyrings) => {
this.keyrings = keyrings
this.setUnlocked()
return this.fullUpdate()
})
}
/**
* Update Memstore Keyrings
*
* Updates the in-memory keyrings, without persisting.
*/
async _updateMemStoreKeyrings () {
const keyrings = await Promise.all(this.keyrings.map(this.displayForKeyring))
return this.memStore.updateState({ keyrings })
}
encryptor
- KeyringController 클래스 내에서 암호화 및 복호화 작업은 암호화 객체에 의해 수행.
Ex1. 키링 데이터를 암호화
return this.encryptor.encrypt(this.password, serializedKeyrings)
Ex2. 암호화된 볼트 복호화
const vault = await this.encryptor.decrypt(password, encryptedVault)
- 암호화 객체는 KeyringController 생성자에서 할당됨.
constructor (opts) {
super();
const initState = opts.initState || {};
this.keyringTypes = opts.keyringTypes ? keyringTypes.concat(opts.keyringTypes) : keyringTypes;
this.store = new ObservableStore(initState);
this.memStore = new ObservableStore({
isUnlocked: false,
keyringTypes: this.keyringTypes.map((krt: T) => krt.type),
keyrings: [],
});
this.encryptor = opts.encryptor || encryptor;
this.keyrings = [];
}
- 크롬 확장 프로그램과 모바일 앱에서는 다른 암호화 툴이 사용됨.
- 확장 프로그램에서는 브라우저-암호화 모듈 사용
- 모바일 앱에서는 자체 암호화 클래스 사용(PBKDF2 반복과 AES 모드 제외 비슷하게 작동) - browser-pasworder:
Generate enc_key from password: PBKDF2, 10000 iteration
AES mode: AES-GCM - Mobile app encryptor:
Generate enc_key from password: PBKDF2, 5000 iteration
AES mode: AES-CBC
Mobile App
- 확장 프로그램과 비슷하게 모바일 앱에서도 키링 구조 사용
- 데이터 영구 저장을 위해 async-storage라는 모듈을 사용하여 데이터 암호화
- ‘Remember me', 'unlock with touch ID/device passcode’ 옵션 제공
→ react-native-keychain을 기반으로 한 SecureKeychain 모듈을 이용해 앱에 비밀번호 저장 - react-native-keychain 데이터 핸들링
- iOS: iOS Keychain을 이용한 데이터 저장
- Android: Android Keystore를 이용하여 데이터 암호화, 암호화된 데이터는 SharedPreferences에 저장
SecureKeychain
- SecureKeychain 설명
/**
* Class that wraps Keychain from react-native-keychain
* abstracting metamask specific functionality and settings
* and also adding an extra layer of encryption before writing into
* the phone's keychain
*/
class SecureKeychain {
...
}
- abstracting metamask specific functionality and settings는 위에서 언급한 ‘Remember me’, ‘unlock with touch ID/device passcode’를 의미.
async resetGenericPassword() {
const options = { service: defaultOptions.service };
await AsyncStorage.removeItem(BIOMETRY_CHOICE);
await AsyncStorage.removeItem(PASSCODE_CHOICE);
return Keychain.resetGenericPassword(options);
}
async getGenericPassword() {
if (instance) {
instance.isAuthenticating = true;
const keychainObject = await Keychain.getGenericPassword(defaultOptions);
if (keychainObject.password) {
const encryptedPassword = keychainObject.password;
const decrypted = await instance.decryptPassword(encryptedPassword);
keychainObject.password = decrypted.password;
instance.isAuthenticating = false;
return keychainObject;
}
instance.isAuthenticating = false;
}
return null;
}
async setGenericPassword(password, type) {
const authOptions = {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
};
if (type === this.TYPES.BIOMETRICS) {
authOptions.accessControl = Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET;
} else if (type === this.TYPES.PASSCODE) {
authOptions.accessControl = Keychain.ACCESS_CONTROL.DEVICE_PASSCODE;
} else if (type === this.TYPES.REMEMBER_ME) {
//Don't need to add any parameter
} else {
// Setting a password without a type does not save it
return await this.resetGenericPassword();
}
const encryptedPassword = await instance.encryptPassword(password);
await Keychain.setGenericPassword('metamask-user', encryptedPassword, { ...defaultOptions, ...authOptions });
if (type === this.TYPES.BIOMETRICS) {
await AsyncStorage.setItem(BIOMETRY_CHOICE, TRUE);
await AsyncStorage.setItem(PASSCODE_DISABLED, TRUE);
await AsyncStorage.removeItem(PASSCODE_CHOICE);
await AsyncStorage.removeItem(BIOMETRY_CHOICE_DISABLED);
// If the user enables biometrics, we're trying to read the password
// immediately so we get the permission prompt
if (Platform.OS === 'ios') {
await this.getGenericPassword();
}
} else if (type === this.TYPES.PASSCODE) {
await AsyncStorage.removeItem(BIOMETRY_CHOICE);
await AsyncStorage.removeItem(PASSCODE_DISABLED);
await AsyncStorage.setItem(PASSCODE_CHOICE, TRUE);
await AsyncStorage.setItem(BIOMETRY_CHOICE_DISABLED, TRUE);
} else if (type === this.TYPES.REMEMBER_ME) {
await AsyncStorage.removeItem(BIOMETRY_CHOICE);
await AsyncStorage.setItem(PASSCODE_DISABLED, TRUE);
await AsyncStorage.removeItem(PASSCODE_CHOICE);
await AsyncStorage.setItem(BIOMETRY_CHOICE_DISABLED, TRUE);
//Don't need to add any parameter
}
}
지갑 생성관련 code path
1. metamask-extension/ui/app/pages/first-time-flow/first-time-flow.component.js
const seedPhrase = await createNewAccount(password)
2. metamask-extension/ui/app/pages/first-time-flow/first-time-flow.container.js
createNewAccount: (password) =>
dispatch(createNewVaultAndGetSeedPhrase(password))
3. metamask-extension/ui/app/store/actions.js
export function createNewVaultAndGetSeedPhrase(password){
....
await createNewVault(password)
const seedWords = await verifySeedPhrase()
....
}
4. metamask-extension/app/scripts/metamask-controller.js
async createNewVaultAndKeychain(password){
vault = await this.keyringController.createNewVaultAndKeychain(password)
}
5. KeyringController/blob/master/index.js
createNewVaultAndKeychain (password) {
return this.persistAllKeyrings(password)
.then(this.createFirstKeyTree.bind(this))
.then(this.persistAllKeyrings.bind(this, password))
.then(this.setUnlocked.bind(this))
.then(this.fullUpdate.bind(this))
}
6. MetaMask/KeyringController/blob/master/index.js
createFirstKeyTree () {
...
return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
...
}
7. MetaMask/KeyringController/blob/master/index.js addNewKeyring (type, opts) {
...
const Keyring = this.getKeyringClassForType(type)
const keyring = new Keyring(opts)
...
}
8. MetaMask/eth-hd-keyring/blob/master/index.js
addAccounts (numberOfAccounts = 1) {
...
this._initFromMnemonic(bip39.generateMnemonic())
...
}
'Blockchain' 카테고리의 다른 글
블록체인 기반 서비스 개인키 관리 가이드라인 (0) | 2023.10.16 |
---|---|
[번역] NFT가 생겨난 이유 (0) | 2023.04.06 |
[번역] NFT는 무엇인가 (0) | 2023.04.04 |