Blockchain

MetaMask 개인키(니모닉) 저장 방식

2pandi 2023. 11. 13. 13:01

키링(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.

 

keyring structure

 

* 키링: 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())
    ...
}