#
Junjie
昨天 bd6b518aae61608ddc2d82b43ccc283dc95b9c54
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package com.zy.common.auth;
 
import com.core.common.Cools;
import org.springframework.stereotype.Component;
 
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
@Component
public class PasskeyChallengeManager {
 
    private static final long EXPIRE_MILLIS = 5 * 60 * 1000L;
 
    private final SecureRandom secureRandom = new SecureRandom();
    private final ConcurrentHashMap<String, ChallengeState> holders = new ConcurrentHashMap<>();
 
    public ChallengeState createRegistration(Long userId, String origin, String rpId) {
        return create(Purpose.REGISTRATION, userId, origin, rpId);
    }
 
    public ChallengeState createAuthentication(Long userId, String origin, String rpId) {
        return create(Purpose.AUTHENTICATION, userId, origin, rpId);
    }
 
    public ChallengeState get(String ticket, Purpose purpose) {
        if (Cools.isEmpty(ticket) || purpose == null) {
            return null;
        }
        ChallengeState state = holders.get(ticket);
        if (state == null) {
            return null;
        }
        if (state.expireAt < System.currentTimeMillis() || state.purpose != purpose) {
            holders.remove(ticket);
            return null;
        }
        return state;
    }
 
    public void remove(String ticket) {
        if (!Cools.isEmpty(ticket)) {
            holders.remove(ticket);
        }
    }
 
    private ChallengeState create(Purpose purpose, Long userId, String origin, String rpId) {
        cleanup();
        ChallengeState state;
        do {
            state = new ChallengeState(
                    randomTicket(),
                    randomChallenge(),
                    purpose,
                    userId,
                    origin,
                    rpId,
                    System.currentTimeMillis() + EXPIRE_MILLIS
            );
        } while (holders.putIfAbsent(state.ticket, state) != null);
        return state;
    }
 
    private void cleanup() {
        long now = System.currentTimeMillis();
        for (Map.Entry<String, ChallengeState> entry : holders.entrySet()) {
            if (entry.getValue().expireAt < now) {
                holders.remove(entry.getKey());
            }
        }
    }
 
    private String randomTicket() {
        byte[] bytes = new byte[24];
        secureRandom.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
 
    private String randomChallenge() {
        byte[] bytes = new byte[32];
        secureRandom.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
 
    public enum Purpose {
        REGISTRATION,
        AUTHENTICATION
    }
 
    public static final class ChallengeState {
        private final String ticket;
        private final String challenge;
        private final Purpose purpose;
        private final Long userId;
        private final String origin;
        private final String rpId;
        private final long expireAt;
 
        private ChallengeState(String ticket, String challenge, Purpose purpose, Long userId, String origin, String rpId, long expireAt) {
            this.ticket = ticket;
            this.challenge = challenge;
            this.purpose = purpose;
            this.userId = userId;
            this.origin = origin;
            this.rpId = rpId;
            this.expireAt = expireAt;
        }
 
        public String getTicket() {
            return ticket;
        }
 
        public String getChallenge() {
            return challenge;
        }
 
        public Purpose getPurpose() {
            return purpose;
        }
 
        public Long getUserId() {
            return userId;
        }
 
        public String getOrigin() {
            return origin;
        }
 
        public String getRpId() {
            return rpId;
        }
 
        public long getExpireAt() {
            return expireAt;
        }
    }
}