Blockchain

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

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())
    ...
}

'Blockchain' 카테고리의 다른 글

블록체인 기반 서비스 개인키 관리 가이드라인  (0) 2023.10.16
[번역] NFT가 생겨난 이유  (0) 2023.04.06
[번역] NFT는 무엇인가  (0) 2023.04.04
'Blockchain' 카테고리의 다른 글
  • 블록체인 기반 서비스 개인키 관리 가이드라인
  • [번역] NFT가 생겨난 이유
  • [번역] NFT는 무엇인가
2pandi
2pandi
웹 프론트엔드 개발자 이예빈입니다.
2pandi
2pandi
2pandi
전체
오늘
어제
  • 분류 전체보기 (21)
    • Next.js (2)
    • React (2)
    • TypeScript (3)
    • JavaScript (1)
    • CSS (1)
    • 항해플러스 (5)
    • Blockchain (4)
    • 기타 (3)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • typescript설정
  • 덕타이핑
  • 항해99
  • nft
  • Next.js
  • React
  • TypeScript
  • 항해플러스
  • tsconfig
  • 블록체인
  • strictNullChecks
  • noImplicitAny
  • 구조적타이핑

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.1.4
2pandi
MetaMask 개인키(니모닉) 저장 방식
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.