클라이언트단에서 서버단으로 정보보낼 때 중간에 password 탈취에 대응하기위해
RSA를 적용해서 password 암호화를 해주었다
RSA에 대해 내가 쓴 글
RSA
RSA란? 현재 SSL/TLS에 가장 많이 사용되는 공개키 암호화 알고리즘 전세계 대부분의 인터넷 뱅킹에서 사용 대칭키가 아닌 공개키와 개인키가 한 쌍을 이룸 공개키로 암호화한 내용은 개인키로만
fullfish.tistory.com
회원가입 흐름
- 클라이언트에서 email과 nickname 그리고 createKey: true(키 생성위해)를 보냄
- 서버에서 이메일 중복확인
- 중복이면 중복 에러
- 중복아니면 키를 생성
- 서버에서 첫 요청시와 그 다음 요청시에 값을 다 입력했는지 검증
- db에 d,e,N저장 (email과 nickname ,d , e, N 있는 유저 생성)
- 클라이언트로 공개키 (e, N)보냄
- 클라이언트에서 password 암호화
- 서버로 password 암호화한것과 email보냄
- 서버에서 아까 email을 이용해서 키들 받아옴
- 서버에서 복호화한후 Bcrypt를 이용해서 hashing하고 db에 아직 null값인 password에 저장
코드
// 클라이언트
export const signUpApi = async (email, nickname, password) => {
const res = await signCustomApi.post('up', {
createKey: true,
nickname,
email,
});
let encrypted = [];
const e = BigInt(Number(JSON.parse(res.data.data.e)));
const N = BigInt(Number(JSON.parse(res.data.data.N)));
BigInt.prototype.toJSON = function () {
return this.toString();
};
console.time('암호화');
for (let i = 0; i < password.length; i++) {
let a = BigInt(password[i].charCodeAt(0));
encrypted[i] = JSON.stringify(power(a, e, N));
}
console.timeEnd('암호화');
const res2 = await signCustomApi.post('up', {
email,
nickname,
password: encrypted,
});
return res2;
};
export const signInApi = async (email, password) => {
const result = await signCustomApi.post('in', {
email,
password,
});
try {
if (result.data.accessToken) {
sessionStorage.setItem('user', JSON.stringify(result.data));
}
return result.data;
} catch (err) {
console.log(err);
}
};
// 서버
up: {
post: async (req, res) => {
try {
const { email, nickname, createKey } = req.body;
let password = req.body.password;
const userInfo = await user.findOne({
where: {
email,
},
});
// 가입 안되어 있을 경우 키 생성
if (createKey === true) {
// 이미 가입되었을 경우
if (userInfo) {
await slack.slack("Signup Post 409");
return res.status(409).send({ message: "email already exists" });
}
let [e, N, d] = RSA.createKey();
const payload = {
email,
nickname,
password: "temp",
d,
e,
N,
};
await user.create(payload);
BigInt.prototype.toJSON = function () {
return this.toString();
};
e = JSON.stringify(e);
N = JSON.stringify(N);
return res.status(201).send({ data: { e: e, N: N } });
} else {
if (!email || !password || !nickname) {
await slack.slack("Signup Post 422");
return res.status(422).send({ message: "insufficient parameters supplied" });
}
let passwordBigIntArr = [];
console.time("복호화");
for (let i = 0; i < password.length; i++) {
passwordBigIntArr[i] = BigInt(Number(JSON.parse(password[i])));
}
console.timeEnd("복호화");
let d = BigInt(userInfo.dataValues.d);
let N = BigInt(userInfo.dataValues.N);
const passwordDecryptedArr = passwordBigIntArr.map((ele) => {
return String.fromCharCode(Number(power(ele, d, N)));
});
password = passwordDecryptedArr.join("");
// //? 방법 1 salt 생성 후 소금 치기
bcrypt.genSalt(13, async function (err, salt) {
bcrypt.hash(password, salt, async function (err, hash) {
userInfo.password = hash;
const result = await userInfo.save();
await slack.slack("User Post 201", `id : ${result.dataValues.id}`);
return res.status(201).send({ data: { id: result.dataValues.id } });
});
});
// //? 방법 2 salt 자동 생성
// bcrypt.hash(password, 13, async function (err, hash) {
// userInfo.password = hash;
// const result = await userInfo.save();
// await slack.slack("User Post 201", `id : ${result.dataValues.id}`);
// return res.status(201).send({ data: { id: result.dataValues.id } });
// });
// //? ---
}
} catch (err) {
await slack.slack("Signup Post 501");
res.status(501).send("Signup Post");
}
},
},
데이터 흐름이
클라이언트 -> 서버 -> 클라이언트 -> 서버 -> 클라이언트이다
공개키를 클라이언트가 가지고 있으면 더욱 더 좋겠지만 s3에 저장하는것에 어려움을 느껴 db에 저장했다
BigInt 타입은 서버와 클라이언트간에 전송이 안되어서
BigInt.prototype.toJSON = function () {
return this.toString();
};
를 이용해서 json화 시켜주었으며 받은 쪽에서는 다시 BigInt화 시켜주었다
로그인 흐름
- 클라이언트에서 이메일과 키확인용(공개키 받기위해) 보냄
- 서버에서 이메일 존재 확인
- 없으면 에러
- 있으면 클라이언트로 e, N 보냄
- 서버에서 첫 요청시와 그 다음 요청시에 값을 다 입력했는지 검증
- 클라이언트에서 password 암호화
- 서버로 password암호화한것과 email보냄
- 서버에서 email 이용해서 개인키 받아옴
- 복호화함
- Bcrypt검증해서 틀리면 비번이 틀린거
- 맞으면 로그인
코드
// 클라이언트
export const signInApi = async (email, password) => {
const res = await signCustomApi.post('in', {
checkKey: true,
email,
});
let encrypted = [];
const e = BigInt(Number(JSON.parse(res.data.data.e)));
const N = BigInt(Number(JSON.parse(res.data.data.N)));
BigInt.prototype.toJSON = function () {
return this.toString();
};
for (let i = 0; i < password.length; i++) {
let a = BigInt(password[i].charCodeAt(0));
encrypted[i] = JSON.stringify(power(a, e, N));
}
const res2 = await signCustomApi.post('in', {
email,
password: encrypted,
});
try {
if (res2.data.accessToken) {
sessionStorage.setItem('user', JSON.stringify(res2.data));
}
return res2.data;
} catch (err) {
console.log(err);
}
};
// 서버
in: {
post: async (req, res) => {
try {
const { email, checkKey } = req.body;
let password = req.body.password;
if (!email) {
await slack.slack("Signin Post 422");
return res.status(422).send({ message: "insufficient parameters supplied" });
}
const userInfo = await user.findOne({
where: {
email,
},
});
//데이터베이스에 email이 없을때
if (!userInfo) {
await slack.slack("Signin Post 400");
return res.status(400).send({ message: "Wrong email" });
}
let e = 0;
let N = 0;
if (checkKey === true) {
e = userInfo.dataValues.e;
N = userInfo.dataValues.N;
return res.status(200).send({ data: { e: e, N: N } });
} else {
let passwordBigIntArr = [];
for (let i = 0; i < password.length; i++) {
passwordBigIntArr[i] = BigInt(Number(JSON.parse(password[i])));
}
let d = BigInt(userInfo.dataValues.d);
let N = BigInt(userInfo.dataValues.N);
const passwordDecryptedArr = passwordBigIntArr.map((ele) => {
return String.fromCharCode(Number(power(ele, d, N)));
});
password = passwordDecryptedArr.join("");
bcrypt.compare(password, userInfo.password, async function (err, result) {
//데이터베이스에 email 있지만 비밀번호가 틀릴때
if (result === false) {
await slack.slack("Signin Post 400");
return res.status(400).send({ message: "Wrong password" });
}
//데이터 베이스에 회원정보가 있을 경우
else {
const payload = {
id: userInfo.id,
email,
};
const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, {
expiresIn: "30m",
});
const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET, {
expiresIn: "6h",
});
res.cookie("refreshToken", refreshToken, {
sameSite: "Strict",
httpOnly: true,
secure: false, // https로 바꾼후에 true로 바꿔야함
});
await slack.slack("Signin Post 200", `id : ${userInfo.id}`);
res.status(200).send({
data: { id: userInfo.id },
accessToken: accessToken,
});
}
});
}
} catch (err) {
await slack.slack("Signin Post 501");
res.status(501).send("Signin Post");
}
},
},
'Project > codestates-final-project' 카테고리의 다른 글
리펙토링 및 개선 - 6 / 환율 적용 (0) | 2022.06.07 |
---|---|
리펙토링 및 개선 - 5 / 카이사르, 모노알파베틱 암호화 적용 (0) | 2022.06.06 |
리펙토링 및 개선 - 3 / Bcrypt 적용 (0) | 2022.05.31 |
리펙토링 및 개선 - 2 / 잡다한 버그 해결 (0) | 2022.05.31 |
리펙토링 및 개선 - 1 / 계획수립 (0) | 2022.05.21 |