001/*
002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Thomas Roger
018 */
019
020package org.nuxeo.wopi;
021
022import java.math.BigInteger;
023import java.nio.ByteBuffer;
024import java.nio.charset.StandardCharsets;
025import java.security.GeneralSecurityException;
026import java.security.KeyFactory;
027import java.security.PublicKey;
028import java.security.Signature;
029import java.security.spec.KeySpec;
030import java.security.spec.RSAPublicKeySpec;
031import java.time.Duration;
032import java.time.Instant;
033
034import javax.xml.bind.DatatypeConverter;
035
036import org.nuxeo.ecm.core.api.NuxeoException;
037
038/**
039 * Proof key helper class.
040 * <p>
041 * See <a href="https://wopi.readthedocs.io/en/latest/scenarios/proofkeys.html"></a>.
042 * <p>
043 * See <a href=
044 * "https://github.com/Microsoft/Office-Online-Test-Tools-and-Documentation/blob/master/samples/java/ProofKeyTester.java"></a>
045 *
046 * @since 10.3
047 */
048public class ProofKeyHelper {
049
050    public static final String KEY_FACTORY_ALGORITHM = "RSA";
051
052    public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
053
054    public static final long EPOCH_IN_TICKS = 621355968000000000L; // January 1, 1970 (start of Unix epoch) in "ticks"
055
056    private ProofKeyHelper() {
057        // helper class
058    }
059
060    public static PublicKey getPublicKey(String modulus, String exponent) {
061        BigInteger mod = new BigInteger(1, DatatypeConverter.parseBase64Binary(modulus));
062        BigInteger exp = new BigInteger(1, DatatypeConverter.parseBase64Binary(exponent));
063        KeyFactory factory;
064        try {
065            factory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
066            KeySpec ks = new RSAPublicKeySpec(mod, exp);
067            return factory.generatePublic(ks);
068        } catch (GeneralSecurityException e) {
069            throw new NuxeoException(e);
070        }
071    }
072
073    public static byte[] getExpectedProofBytes(String url, String accessToken, long timestamp) {
074        byte[] accessTokenBytes = accessToken.getBytes(StandardCharsets.UTF_8);
075        byte[] hostUrlBytes = url.toUpperCase().getBytes(StandardCharsets.UTF_8);
076        ByteBuffer byteBuffer = ByteBuffer.allocate(4 + accessTokenBytes.length + 4 + hostUrlBytes.length + 4 + 8);
077        byteBuffer.putInt(accessTokenBytes.length);
078        byteBuffer.put(accessTokenBytes);
079        byteBuffer.putInt(hostUrlBytes.length);
080        byteBuffer.put(hostUrlBytes);
081        byteBuffer.putInt(8);
082        byteBuffer.putLong(timestamp);
083        return byteBuffer.array();
084    }
085
086    public static boolean verifyProofKey(PublicKey key, String proofKeyHeader, byte[] expectedProofBytes) {
087        try {
088            Signature verifier = Signature.getInstance(SIGNATURE_ALGORITHM);
089            verifier.initVerify(key);
090            verifier.update(expectedProofBytes);
091            byte[] signedProof = DatatypeConverter.parseBase64Binary(proofKeyHeader);
092            return verifier.verify(signedProof);
093        } catch (GeneralSecurityException e) {
094            return false;
095        }
096    }
097
098    /**
099     * Checks that the given {@code timestamp} is no more than 20 minutes old.
100     *
101     * @throws NuxeoException if the timestamp is older than 20 minutes
102     */
103    public static boolean verifyTimestamp(long timestamp) {
104        long ticks = timestamp - EPOCH_IN_TICKS; // ticks
105        long ms = ticks / 10_000; // milliseconds
106        Instant instant = Instant.ofEpochMilli(ms);
107        Duration duration = Duration.between(instant, Instant.now());
108        return duration.compareTo(Duration.ofMinutes(20)) <= 0;
109    }
110
111}