diff --git a/src/main/resources/static/js/password-crypto.js b/src/main/resources/static/js/password-crypto.js
index c1eac21..e78ae5d 100644
--- a/src/main/resources/static/js/password-crypto.js
+++ b/src/main/resources/static/js/password-crypto.js
@@ -158,8 +158,86 @@
});
}
+ /** 원본 입력에서 name 을 잠시 떼어둔 동안 그 이름을 보관하는 표시용 속성 */
+ var NAME_HOLDER = 'data-password-crypto-name';
+
/**
- * 폼의 지정 필드 값을 봉투로 치환한다. 실패해도 reject 하지 않고 평문을 남긴다.
+ * 입력 요소에서 실제 하나를 고른다.
+ * 같은 name 이 둘 이상이면 form.elements[name] 이 RadioNodeList 를 돌려주므로,
+ * 우리가 붙인 hidden 이 아닌 실제 입력창을 골라야 한다.
+ */
+ function resolveInput(el) {
+ if (!el) {
+ return null;
+ }
+ if (el.tagName) {
+ return el;
+ }
+ for (var i = 0; i < el.length; i++) {
+ var candidate = el[i];
+ if (candidate && candidate.tagName && candidate.type !== 'hidden') {
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 원본 입력의 value 는 그대로 두고, name 만 같은 이름의 hidden 으로 옮겨 봉투를 싣는다.
+ * @returns {Function} 원상복구 함수
+ */
+ function swapToEnvelopeField(form, input, name, envelope) {
+ var hidden = (form.ownerDocument || global.document).createElement('input');
+ hidden.type = 'hidden';
+ hidden.name = name;
+ hidden.value = envelope;
+ hidden.setAttribute(NAME_HOLDER, name);
+
+ input.setAttribute(NAME_HOLDER, name);
+ input.removeAttribute('name');
+ form.appendChild(hidden);
+
+ return function restore() {
+ if (hidden.parentNode) {
+ hidden.parentNode.removeChild(hidden);
+ }
+ if (input.getAttribute(NAME_HOLDER) === name) {
+ input.setAttribute('name', name);
+ input.removeAttribute(NAME_HOLDER);
+ }
+ };
+ }
+
+ /**
+ * 제출 직후 원상복구한다. 폼 데이터 직렬화는 form.submit()/requestSubmit() 호출 시점에
+ * 동기로 끝나므로, 다음 매크로태스크에서 되돌려도 봉투는 이미 전송된 뒤다.
+ * 뒤로가기(bfcache) 복원 시에도 name 이 떨어진 채 남지 않도록 pageshow 에서 한 번 더 처리한다.
+ */
+ function scheduleRestore(restores) {
+ if (restores.length === 0) {
+ return;
+ }
+ var done = false;
+ var run = function () {
+ if (done) {
+ return;
+ }
+ done = true;
+ restores.forEach(function (restore) { restore(); });
+ global.removeEventListener('pageshow', run);
+ };
+ global.addEventListener('pageshow', run);
+ global.setTimeout(run, 0);
+ }
+
+ /**
+ * 폼의 지정 필드를 봉투로 전송한다. 실패해도 reject 하지 않고 평문을 남긴다.
+ *
+ * 입력창()의 value 는 절대 건드리지 않는다. 브라우저 비밀번호 관리자는
+ * 제출 시점의 입력값을 그대로 저장하므로 봉투로 덮어쓰면 봉투 문자열이 비밀번호로 저장되고,
+ * 다음 자동완성 로그인이 깨진다. 대신 원본 입력의 name 을 잠시 떼고 같은 name 의 hidden 에
+ * 봉투를 실어 보낸 뒤 제출 직후 되돌린다.
+ *
* @param {HTMLFormElement} form
* @param {string[]} fieldNames
* @returns {Promise}
@@ -171,7 +249,7 @@
var targets = {};
var elements = {};
fieldNames.forEach(function (name) {
- var el = form.elements ? form.elements[name] : null;
+ var el = resolveInput(form.elements ? form.elements[name] : null);
if (el && typeof el.value === 'string' && el.value.length > 0) {
elements[name] = el;
targets[name] = el.value;
@@ -181,11 +259,14 @@
return global.Promise.resolve();
}
return encryptValues(targets).then(function (encrypted) {
+ var restores = [];
Object.keys(elements).forEach(function (name) {
- if (encrypted[name]) {
- elements[name].value = encrypted[name];
+ // 폴백으로 평문이 그대로 돌아온 경우엔 아무것도 바꾸지 않는다.
+ if (encrypted[name] && encrypted[name] !== targets[name]) {
+ restores.push(swapToEnvelopeField(form, elements[name], name, encrypted[name]));
}
});
+ scheduleRestore(restores);
});
}