Xplentyで個人情報のマスキング方法

近年、社会的に個人情報の取り扱いが厳しくなっております。
社外向けのみならず社内向けの情報転送にも、当たり前のように個人情報の漏えいがないように気を付けています。

システムの高可用性を維持するために導入したマイクロサービスアーキテクチャは、その特徴によって同じ情報（形も意味も含めて）を複数のシステムでそれぞれ持つようになりました。アーキテクチャの特徴による個人情報を含めた様々な重複情報の存在は、データベース１箇所で集約したモノリシックアーキテクチャより遥かに頻雑になりました。

また、AIやデータサイエンスの台頭によるデータ分析基盤の構築で会社が扱っているデータの１箇所への集約も大きな課題になっています。

弊社のXplentyでは、多数のシステムに散在している様々な形の情報を取り集め、集計や結合などのデータ変換で高付加価値の情報への転換に適したSaaS型サービスです。
その点を有効活用によって会社として特定の個人情報に一貫した保護策を講じられます。

今回のブログでは、シャッフル、部分隠し、暗号化による個人情報の保護策をXplentyでどう実装するかを見てみましょう。

## 個人情報マスキングの種類
個人情報の保護は、下記の３種類の方法があります。各々の方法には長所と短所があり、用途に沿って使う必要があります。

1. シャッフル
    * データの文字や数字を置き換える、または順番を変えること
    * 本番データをテストデータとして提供する場合に有効
    * 意味を持つ部分の置き換えは、意味を維持しつつ変換するための工夫が必要
2. 部分隠し
    * データの一部分を無意味な文字に置き換えること
    * フォーマットは維持と特定の部分（元の値を置き換えが必要）のみを見せる場合に有効
    * シャッフルと同じく意味を持つ部分の置き換えは工夫が必要
3. 暗号化
    * 単方向ハッシュ関数を使い、別のデータに変換すること
    * 種類が少ないデータの原型が把握出来なくする場合に有効
    * 復号化が出来ない関数を使うのうが大事である

## 個人情報のマスキングの実行箇所
個人情報の保護について実装箇所は、主にselectコンポーネントにて行われます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65461/276be5bb-3954-aa6b-36d3-a0abd51510d2.png)

selectコンポーネントの中を覗いてみると下記の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65461/96853789-0cad-7e73-3729-e49e73cb1ed8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65461/bfac296e-a67c-1608-1bf8-5ee5a6a587a8.png)

また、各々のフィールドの設定は、図の赤の矢印が示している部分をクリックすると現れる下記のExpression Editor上で行われます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65461/062a81d8-8716-30b3-84e3-98e20aa3ccf5.png)
もちろん、Expressionに該当関数式をコピペしても問題ありません。


## 個人情報のマスキングの例
例に使われているデータは、すべてSmartHRのサンドボックス環境から取得したものです。
SmartHRのサンドボックス環境やAPIについては、[SmartHR API Specifications](https://developer.smarthr.jp/api/index.html)をご参照頂ければ幸いです。

### **社員番号(数字のみ)**

|元のデータ|シャッフル|部分隠し|
|:---:|:---:|:---:|
|00008|00903|##008|
|00023|03103|0##23|
|00026|03104|000##|
|00025|03304|000##|
|00017|02502|##017|
|00006|01400|000##|
|00001|00104|##001|
|00024|03201|##024|
|00019|02701|##019|
|00018|02504|0##18|

```
● シャッフル
SPRINTF('%03d%02d',
	(int)SUBSTRING(emp_code, 3, Length(emp_code)) + (ROUND(RANDOM()*10) % 10),
	(int)SUBSTRING(emp_code, 0, 3) + (ROUND(RANDOM()*10) % 10)
)

● 部分隠し
CASE ROUND(RANDOM() * 10) % 4 
WHEN 1 THEN 
	CONCAT(SUBSTRING(emp_code, 0, Length(emp_code)-2), '##')
WHEN 2 THEN 
	CONCAT(SUBSTRING(emp_code, 0, 1), '##', SUBSTRING(emp_code, Length(emp_code)-2, Length(emp_code)))
WHEN 3 THEN 
	CONCAT(SUBSTRING(emp_code, 0, 2), '##', SUBSTRING(emp_code, Length(emp_code)-1, Length(emp_code)))
ELSE
	CONCAT('##', SUBSTRING(emp_code, 2, Length(emp_code)))
END
```


### **名前(漢字)**

|元のデータ||シャッフル||部分隠し|
|:---:|:---:|:---:|:---:|:---:|
|Last Name|First Name|Last Name|First Name|Last Name|First Name|
|石井|玲佳|玲井|石佳|#井|玲#|
|古賀|祐奈|古奈|祐賀|#賀|#奈|
|菊池|舞麗|菊麗|舞池|菊#|舞#|
|湯浅|海琴|海浅|海浅|湯#|海#|
|高松|暢|松|高暢|高#|暢|
|河野|咲枝|咲野|河枝|河#|咲#|
|上野|真綾|真野|上綾|#野|真#|
|小山|泰治|泰山|小治|小#|泰#|
|大場|孝浩|大浩|大浩|#場|孝#|
|松原|敏雄|敏原|松雄|松#|#雄|

```
● シャッフル(last_name, first_name)
CASE ROUND(RANDOM() * 10) % 2
WHEN 0 THEN
	CONCAT(SUBSTRING(last_name, 0, 1), SUBSTRING(first_name, 1, Length(first_name)))
ELSE
	CONCAT(SUBSTRING(Reverse(first_name), 1, Length(first_name)), SUBSTRING(Reverse(last_name), 0, 1))
END

CASE ROUND(RANDOM() * 10) % 2
WHEN 1 THEN
	CONCAT(SUBSTRING(first_name, 0, 1), SUBSTRING(last_name, 1, Length(last_name)))
ELSE
	CONCAT( SUBSTRING(Reverse(last_name), 1, Length(last_name)), SUBSTRING(Reverse(first_name), 0, 1))
END

● 部分隠し(last_name, first_name)
CASE ROUND(RANDOM() * 10) % 2
WHEN 0 THEN
	SPRINTF('%s%s', SUBSTRING(last_name, 0, 1), REPLACE(SUBSTRING(last_name, 1, Length(last_name)), '.', '#'))
ELSE
	SPRINTF('%s%s', REPLACE(SUBSTRING(last_name, 0, Length(last_name)-1), '.', '#'), SUBSTRING(last_name, Length(last_name)-1, Length(last_name)))
END

CASE ROUND(RANDOM() * 10) % 2
WHEN 1
	THEN SPRINTF('%s%s', SUBSTRING(first_name, 0, 1), REPLACE(SUBSTRING(first_name, 1, Length(first_name)), '.', '#'))
ELSE
	SPRINTF('%s%s', REPLACE(SUBSTRING(first_name, 0, Length(first_name)-1), '.', '#'), SUBSTRING(first_name, Length(first_name)-1, Length(first_name)))
END
```

### **誕生日(年月日)**
|元のデータ|部分隠し|
|:---:|:---:|
|1948-12-22|`19**-**-**`|

```
● 部分隠し
CONCAT(
    SUBSTRING(birth_at, 0, 2), 
    REPLACE(SUBSTRING(birth_at, 2, Length(birth_at)), '[0-9]', '*')
)
```

### **性別**

|元のデータ|部分隠し|
|:---:|:---:|
|male|07cf4|
|female|273b9|

```
● 暗号化
SUBSTRING(MD5(gender), 0, 5)

--> MD5の代わりにSHA256などの他の単方向暗号化関数でも良い
```

### **電話番号**
|元のデータ|シャッフル|部分隠し|
|:---|:---|:---|
|090-5302-5398|090-2053-8953|`090-****-****`|
|070-4596-8665|070-6945-5686|`070-****-****`|
|070-7119-7796|070-9171-6977|`070-****-****`|
|090-3071-1160|090-1730-0611|`090-****-****`|
|070-1688-0980|070-8816-0809|`070-****-****`|
|090-8426-7235|090-6284-5372|`090-****-****`|
|070-7213-2746|070-3172-6427|`070-****-****`|
|090-8151-8277|090-1581-7782|`090-****-****`|
|090-3304-2880|090-4033-0828|`090-****-****`|
|080-5377-6182|080-7753-2861|`080-****-****`|

```
● シャッフル
SPRINTF('%s-%s%s-%s%s',
	STRSPLIT(tel_number, '-').$0,
	SUBSTRING(Reverse(STRSPLIT(tel_number, '-').$1), 0, 2), 
	SUBSTRING(STRSPLIT(tel_number, '-').$1, 0, 2),
	SUBSTRING(Reverse(STRSPLIT(tel_number, '-').$2), 0, 2),
	SUBSTRING(STRSPLIT(tel_number, '-').$2, 0, 2)
)

● 部分隠し
CONCAT(STRSPLIT(tel_number, '-').$0, '-',
	REPLACE(STRSPLIT(tel_number, '-').$1, '[0-9]', '*'),'-', 
	REPLACE(STRSPLIT(tel_number, '-').$2, '[0-9]', '*')
)
```

### **郵便番号**
|元のデータ|部分隠し|
|:---|:---|
|567-0896|#6#-#896|
|480-1122|#8#-11##|
|484-0863|4#4-0###|
|414-0026|4#4-002#|
|409-2946|##9-#9#6|
|347-0104|#47-0#04|
|938-0178|9##-0#7#|
|950-1113|9##-1113|
|964-0935|9#4-09##|
|957-0066|9##-##66|

```
● 部分隠し
(ROUND(RANDOM()*10) % 2 == 0 ?
	REPLACE(address#'zip_code', '[13568]' ,'#') :
	REPLACE(address#'zip_code', '[02457]' ,'#')
)
```

### **住所(番地)**

|元のデータ|シャッフル_1|シャッフル_2|部分隠し|
|:---:|:---:|:---:|:---:|
|4-23-11|4-11-23|10-27-14|4-2#-##|
|7-22-7|7-22-7|16-29-9|#-22-#|
|7-5-11|7-11-5|7-14-13|7-5-11|
|1-13-19|1-31-91|6-20-26|#-##-##|
|3-9-29|3-9-92|5-15-37|3-9-#9|
|7-27-30|7-72-3|9-34-39|7-#7-3#|
|8-22-21|8-22-12|8-23-25|8-22-2#|
|6-4-17|6-17-4|13-11-19|6-4-##|
|1-26-16|1-16-26|10-27-23|#-26-#6|
|7-23-10|7-10-23|14-27-19|7-#3-1#|

```
● シャッフル_1
(ROUND(RANDOM()*10) % 2 == 0 ?
	SPRINTF('%s-%d-%d',
		STRSPLIT(address#'street', '-').$0,
		(int)STRSPLIT(address#'street', '-').$2,
		(int)STRSPLIT(address#'street', '-').$1
	) :
	SPRINTF('%s-%d-%d',
		STRSPLIT(address#'street', '-').$0,
		(int)Reverse(STRSPLIT(address#'street', '-').$1),
		(int)Reverse(STRSPLIT(address#'street', '-').$2)
	)
)

● シャッフル_2
SPRINTF('%d-%d-%d',
	(int)STRSPLIT(address#'street', '-').$0 + (ROUND(RANDOM()*10) %10),
	(int)STRSPLIT(address#'street', '-').$1 + (ROUND(RANDOM()*10) %10),
	(int)STRSPLIT(address#'street', '-').$2 + (ROUND(RANDOM()*10) %10)
)

● 部分隠し
(ROUND(RANDOM()*10) % 2 == 0 ?
	REPLACE(address#'street', '[13579]' ,'#') :
	REPLACE(address#'street', '[02468]' ,'#')
)
```

### **メールアドレス**

|元のデータ|部分隠し_1|部分隠し_2|
|:---|:---|:---|
|gaston.nikolaus@example.com|ga*************@######.com|ga*******@######.com|
|salvatore@example.com|sa*******@######.com|sa*******@######.com|
|bruno_abbott@example.net|br**********@######.net|br*******@######.net|
|arnold.shanahan@example.net|ar*************@######.net|ar*******@######.net|
|george@example.net|ge****@######.net|ge*******@######.net|
|celestina@example.org|ce*******@######.org|ce*******@######.org|
|avery.homenick@example.org|av************@######.org|av*******@######.org|
|tyson@example.net|ty***@######.net|ty*******@######.net|
|sang.hirthe@example.org|sa*********@######.org|sa*******@######.org|
|hunter@example.com|hu****@######.com|hu*******@######.com|

```
● 部分隠し_1
SPRINTF('%s%s@%s%s',
	SUBSTRING(STRSPLIT(email, '@').$0, 0, 2),
	REPLACE(SUBSTRING(STRSPLIT(email, '@').$0, 2, Length(STRSPLIT(email, '@').$0)), '.', '*'),
	REPLACE(SUBSTRING(STRSPLIT(email, '@').$1, 0, LAST_INDEX_OF(STRSPLIT(email, '@').$1, '.') - 1), '.', '#' ),
	SUBSTRING(STRSPLIT(email, '@').$1, LAST_INDEX_OF(STRSPLIT(email, '@').$1, '.'), Length(STRSPLIT(email, '@').$1))
)

● 部分隠し_2
SPRINTF('%s%s@%s%s',
	SUBSTRING(STRSPLIT(email, '@').$0, 0, 2),
	'*******',
	'######',
	SUBSTRING(STRSPLIT(email, '@').$1, LAST_INDEX_OF(STRSPLIT(email, '@').$1, '.'), Length(STRSPLIT(email, '@').$1))
)
```

### **各種社会保険番号**

|元のデータ|シャッフル|部分隠し_1|部分隠し_2|部分隠し_3|
|:---|:---|:---|:---|:---|
|5063-196700-3|5107-007833-5|50##-#9#700-#|5**3-1####0-3|`*0@@-@967@@-@`|
|4598-668799-1|4603-997988-2|#59#-###799-1|4**8-6####9-1|`*5@@-@687@@-@`|
|4676-458881-5|4685-189071-8|##7#-#5###1-5|4**6-4####1-5|`*6@@-@588@@-@`|
|5105-769271-1|5180-173803-8|5#05-7#927#-#|5**5-7####1-1|`*1@@-@692@@-@`|
|5323-496653-1|5358-356889-3|53#3-#9##53-1|5**3-4####3-1|`*3@@-@966@@-@`|
|8875-203981-7|8965-189912-7|88##-20##8#-#|8**5-2####1-7|`*8@@-@039@@-@`|
|8845-730489-5|8883-984208-6|###5-73###9-5|8**5-7####9-5|`*8@@-@304@@-@`|
|2765-123674-2|2856-476816-9|27#5-#2##7#-2|2**5-1####4-2|`*7@@-@236@@-@`|
|8968-785929-6|9023-930160-4|8#68-#8####-6|8**8-7####9-6|`*9@@-@859@@-@`|
|4879-952886-9|4923-688784-6|##79-952###-9|4**9-9####6-9|`*8@@-@528@@-@`|

```
● シャッフル
SPRINTF('%04d-%06d-%d',
	((int)STRSPLIT(emp_ins_insured_person_number, '-').$0 + ROUND(RANDOM()*100)) % 10000,
	((int)Reverse(STRSPLIT(emp_ins_insured_person_number, '-').$1) + ROUND(RANDOM()*1000)) % 1000000,
	((int)STRSPLIT(emp_ins_insured_person_number, '-').$2 + ROUND(RANDOM()*10)) % 10
)

● 部分隠し_1
CASE ROUND(RANDOM() * 10) % 4 
WHEN 1 THEN
	REPLACE(emp_ins_insured_person_number, '[13579]', '#')
WHEN 2 THEN
	REPLACE(emp_ins_insured_person_number, '[02579]', '#')
WHEN 3 THEN
	REPLACE(emp_ins_insured_person_number, '[13468]', '#')
ELSE
	REPLACE(emp_ins_insured_person_number, '[02468]', '#')
END

● 部分隠し_2
SPRINTF('%s%s%s-%s%s%s-%s',
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\d)(\\d+)(\\d)-(\\d)(\\d+)(\\d)-(\\d)').$0,
	REPLACE(REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\d)(\\d+)(\\d)-(\\d)(\\d+)(\\d)-(\\d)').$1, '.', '*'),
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\d)(\\d+)(\\d)-(\\d)(\\d+)(\\d)-(\\d)').$2,
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\d)(\\d+)(\\d)-(\\d)(\\d+)(\\d)-(\\d)').$3,
	REPLACE(REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\d)(\\d+)(\\d)-(\\d)(\\d+)(\\d)-(\\d)').$4, '.', '#'),
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\d)(\\d+)(\\d)-(\\d)(\\d+)(\\d)-(\\d)').$5,
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\d)(\\d+)(\\d)-(\\d)(\\d+)(\\d)-(\\d)').$6
)

● 部分隠し_3
REPLACE(REPLACE(emp_ins_insured_person_number, '\\d\\d-\\d', '@@-@'), '^\\d', '*')
```

## まとめ
Xplentyのselectコンポーネントには、このようなマスキング以外にも様々な機能が実装できます。ぜひ、Xplentyの公式マニュアルやX-consoleにて機能を確認してみてはいかがでしょうか？

[無料トライアル申請](https://try.integrate.io/jp-demo/)にていつでもXplentyをお試し頂けます。